LCOV - code coverage report
Current view: top level - source_span-1.8.1/lib/src - highlighter.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 0 299 0.0 %
Date: 2021-11-28 14:37:50 Functions: 0 0 -

          Line data    Source code
       1             : // Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
       2             : // for details. All rights reserved. Use of this source code is governed by a
       3             : // BSD-style license that can be found in the LICENSE file.
       4             : 
       5             : import 'dart:math' as math;
       6             : 
       7             : import 'package:collection/collection.dart';
       8             : import 'package:path/path.dart' as p;
       9             : import 'package:term_glyph/term_glyph.dart' as glyph;
      10             : 
      11             : import 'charcode.dart';
      12             : import 'colors.dart' as colors;
      13             : import 'location.dart';
      14             : import 'span.dart';
      15             : import 'span_with_context.dart';
      16             : import 'utils.dart';
      17             : 
      18             : /// A class for writing a chunk of text with a particular span highlighted.
      19             : class Highlighter {
      20             :   /// The lines to display, including context around the highlighted spans.
      21             :   final List<_Line> _lines;
      22             : 
      23             :   /// The color to highlight the primary [_Highlight] within its context, or
      24             :   /// `null` if it should not be colored.
      25             :   final String? _primaryColor;
      26             : 
      27             :   /// The color to highlight the secondary [_Highlight]s within their context,
      28             :   /// or `null` if they should not be colored.
      29             :   final String? _secondaryColor;
      30             : 
      31             :   /// The number of characters before the bar in the sidebar.
      32             :   final int _paddingBeforeSidebar;
      33             : 
      34             :   /// The maximum number of multiline spans that cover any part of a single
      35             :   /// line in [_lines].
      36             :   final int _maxMultilineSpans;
      37             : 
      38             :   /// Whether [_lines] includes lines from multiple different files.
      39             :   final bool _multipleFiles;
      40             : 
      41             :   /// The buffer to which to write the result.
      42             :   final _buffer = StringBuffer();
      43             : 
      44             :   /// The number of spaces to render for hard tabs that appear in `_span.text`.
      45             :   ///
      46             :   /// We don't want to render raw tabs, because they'll mess up our character
      47             :   /// alignment.
      48             :   static const _spacesPerTab = 4;
      49             : 
      50             :   /// Creates a [Highlighter] that will return a string highlighting [span]
      51             :   /// within the text of its file when [highlight] is called.
      52             :   ///
      53             :   /// [color] may either be a [String], a [bool], or `null`. If it's a string,
      54             :   /// it indicates an [ANSI terminal color escape][] that should be used to
      55             :   /// highlight [span]'s text (for example, `"\u001b[31m"` will color red). If
      56             :   /// it's `true`, it indicates that the text should be highlighted using the
      57             :   /// default color. If it's `false` or `null`, it indicates that no color
      58             :   /// should be used.
      59             :   ///
      60             :   /// [ANSI terminal color escape]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
      61           0 :   Highlighter(SourceSpan span, {color})
      62           0 :       : this._(_collateLines([_Highlight(span, primary: true)]), () {
      63           0 :           if (color == true) return colors.red;
      64           0 :           if (color == false) return null;
      65             :           return color as String?;
      66             :         }(), null);
      67             : 
      68             :   /// Creates a [Highlighter] that will return a string highlighting
      69             :   /// [primarySpan] as well as all the spans in [secondarySpans] within the text
      70             :   /// of their file when [highlight] is called.
      71             :   ///
      72             :   /// Each span has an associated label that will be written alongside it. For
      73             :   /// [primarySpan] this message is [primaryLabel], and for [secondarySpans] the
      74             :   /// labels are the map values.
      75             :   ///
      76             :   /// If [color] is `true`, this will use [ANSI terminal color escapes][] to
      77             :   /// highlight the text. The [primarySpan] will be highlighted with
      78             :   /// [primaryColor] (which defaults to red), and the [secondarySpans] will be
      79             :   /// highlighted with [secondaryColor] (which defaults to blue). These
      80             :   /// arguments are ignored if [color] is `false`.
      81             :   ///
      82             :   /// [ANSI terminal color escape]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
      83           0 :   Highlighter.multiple(SourceSpan primarySpan, String primaryLabel,
      84             :       Map<SourceSpan, String> secondarySpans,
      85             :       {bool color = false, String? primaryColor, String? secondaryColor})
      86           0 :       : this._(
      87           0 :             _collateLines([
      88           0 :               _Highlight(primarySpan, label: primaryLabel, primary: true),
      89           0 :               for (var entry in secondarySpans.entries)
      90           0 :                 _Highlight(entry.key, label: entry.value)
      91             :             ]),
      92             :             color ? (primaryColor ?? colors.red) : null,
      93             :             color ? (secondaryColor ?? colors.blue) : null);
      94             : 
      95           0 :   Highlighter._(this._lines, this._primaryColor, this._secondaryColor)
      96           0 :       : _paddingBeforeSidebar = 1 +
      97           0 :             math.max<int>(
      98             :                 // In a purely mathematical world, floor(log10(n)) would give the
      99             :                 // number of digits in n, but floating point errors render that
     100             :                 // unreliable in practice.
     101           0 :                 (_lines.last.number + 1).toString().length,
     102             :                 // If [_lines] aren't contiguous, we'll write "..." in place of a
     103             :                 // line number.
     104           0 :                 _contiguous(_lines) ? 0 : 3),
     105             :         _maxMultilineSpans = _lines
     106           0 :             .map((line) => line.highlights
     107           0 :                 .where((highlight) => isMultiline(highlight.span))
     108           0 :                 .length)
     109           0 :             .reduce(math.max),
     110           0 :         _multipleFiles = !isAllTheSame(_lines.map((line) => line.url));
     111             : 
     112             :   /// Returns whether [lines] contains any adjacent lines from the same source
     113             :   /// file that aren't adjacent in the original file.
     114           0 :   static bool _contiguous(List<_Line> lines) {
     115           0 :     for (var i = 0; i < lines.length - 1; i++) {
     116           0 :       final thisLine = lines[i];
     117           0 :       final nextLine = lines[i + 1];
     118           0 :       if (thisLine.number + 1 != nextLine.number &&
     119           0 :           thisLine.url == nextLine.url) {
     120             :         return false;
     121             :       }
     122             :     }
     123             :     return true;
     124             :   }
     125             : 
     126             :   /// Collect all the source lines from the contexts of all spans in
     127             :   /// [highlights], and associates them with the highlights that cover them.
     128           0 :   static List<_Line> _collateLines(List<_Highlight> highlights) {
     129           0 :     final highlightsByUrl = groupBy<_Highlight, Uri?>(
     130           0 :         highlights, (highlight) => highlight.span.sourceUrl);
     131           0 :     for (var list in highlightsByUrl.values) {
     132           0 :       list.sort((highlight1, highlight2) =>
     133           0 :           highlight1.span.compareTo(highlight2.span));
     134             :     }
     135             : 
     136           0 :     return highlightsByUrl.values.expand((highlightsForFile) {
     137             :       // First, create a list of all the lines in the current file that we have
     138             :       // context for along with their line numbers.
     139           0 :       final lines = <_Line>[];
     140           0 :       for (var highlight in highlightsForFile) {
     141           0 :         final context = highlight.span.context;
     142             :         // If [highlight.span.context] contains lines prior to the one
     143             :         // [highlight.span.text] appears on, write those first.
     144           0 :         final lineStart = findLineStart(
     145           0 :             context, highlight.span.text, highlight.span.start.column)!;
     146             : 
     147             :         final linesBeforeSpan =
     148           0 :             '\n'.allMatches(context.substring(0, lineStart)).length;
     149             : 
     150           0 :         final url = highlight.span.sourceUrl;
     151           0 :         var lineNumber = highlight.span.start.line - linesBeforeSpan;
     152           0 :         for (var line in context.split('\n')) {
     153             :           // Only add a line if it hasn't already been added for a previous span.
     154           0 :           if (lines.isEmpty || lineNumber > lines.last.number) {
     155           0 :             lines.add(_Line(line, lineNumber, url));
     156             :           }
     157           0 :           lineNumber++;
     158             :         }
     159             :       }
     160             : 
     161             :       // Next, associate each line with each highlights that covers it.
     162           0 :       final activeHighlights = <_Highlight>[];
     163             :       var highlightIndex = 0;
     164           0 :       for (var line in lines) {
     165           0 :         activeHighlights.removeWhere((highlight) =>
     166           0 :             highlight.span.sourceUrl != line.url ||
     167           0 :             highlight.span.end.line < line.number);
     168             : 
     169           0 :         final oldHighlightLength = activeHighlights.length;
     170           0 :         for (var highlight in highlightsForFile.skip(highlightIndex)) {
     171           0 :           if (highlight.span.start.line > line.number) break;
     172           0 :           if (highlight.span.sourceUrl != line.url) break;
     173           0 :           activeHighlights.add(highlight);
     174             :         }
     175           0 :         highlightIndex += activeHighlights.length - oldHighlightLength;
     176             : 
     177           0 :         line.highlights.addAll(activeHighlights);
     178             :       }
     179             : 
     180             :       return lines;
     181           0 :     }).toList();
     182             :   }
     183             : 
     184             :   /// Returns the highlighted span text.
     185             :   ///
     186             :   /// This method should only be called once.
     187           0 :   String highlight() {
     188           0 :     _writeFileStart(_lines.first.url);
     189             : 
     190             :     // Each index of this list represents a column after the sidebar that could
     191             :     // contain a line indicating an active highlight. If it's `null`, that
     192             :     // column is empty; if it contains a highlight, it should be drawn for that column.
     193             :     final highlightsByColumn =
     194           0 :         List<_Highlight?>.filled(_maxMultilineSpans, null);
     195             : 
     196           0 :     for (var i = 0; i < _lines.length; i++) {
     197           0 :       final line = _lines[i];
     198           0 :       if (i > 0) {
     199           0 :         final lastLine = _lines[i - 1];
     200           0 :         if (lastLine.url != line.url) {
     201           0 :           _writeSidebar(end: glyph.upEnd);
     202           0 :           _buffer.writeln();
     203           0 :           _writeFileStart(line.url);
     204           0 :         } else if (lastLine.number + 1 != line.number) {
     205           0 :           _writeSidebar(text: '...');
     206           0 :           _buffer.writeln();
     207             :         }
     208             :       }
     209             : 
     210             :       // If a highlight covers the entire first line other than initial
     211             :       // whitespace, don't bother pointing out exactly where it begins. Iterate
     212             :       // in reverse so that longer highlights (which are sorted after shorter
     213             :       // highlights) appear further out, leading to fewer crossed lines.
     214           0 :       for (var highlight in line.highlights.reversed) {
     215           0 :         if (isMultiline(highlight.span) &&
     216           0 :             highlight.span.start.line == line.number &&
     217           0 :             _isOnlyWhitespace(
     218           0 :                 line.text.substring(0, highlight.span.start.column))) {
     219           0 :           replaceFirstNull(highlightsByColumn, highlight);
     220             :         }
     221             :       }
     222             : 
     223           0 :       _writeSidebar(line: line.number);
     224           0 :       _buffer.write(' ');
     225           0 :       _writeMultilineHighlights(line, highlightsByColumn);
     226           0 :       if (highlightsByColumn.isNotEmpty) _buffer.write(' ');
     227             : 
     228             :       final primaryIdx =
     229           0 :           line.highlights.indexWhere((highlight) => highlight.isPrimary);
     230           0 :       final primary = primaryIdx == -1 ? null : line.highlights[primaryIdx];
     231             : 
     232             :       if (primary != null) {
     233           0 :         _writeHighlightedText(
     234           0 :             line.text,
     235           0 :             primary.span.start.line == line.number
     236           0 :                 ? primary.span.start.column
     237             :                 : 0,
     238           0 :             primary.span.end.line == line.number
     239           0 :                 ? primary.span.end.column
     240           0 :                 : line.text.length,
     241           0 :             color: _primaryColor);
     242             :       } else {
     243           0 :         _writeText(line.text);
     244             :       }
     245           0 :       _buffer.writeln();
     246             : 
     247             :       // Always write the primary span's indicator first so that it's right next
     248             :       // to the highlighted text.
     249           0 :       if (primary != null) _writeIndicator(line, primary, highlightsByColumn);
     250           0 :       for (var highlight in line.highlights) {
     251           0 :         if (highlight.isPrimary) continue;
     252           0 :         _writeIndicator(line, highlight, highlightsByColumn);
     253             :       }
     254             :     }
     255             : 
     256           0 :     _writeSidebar(end: glyph.upEnd);
     257           0 :     return _buffer.toString();
     258             :   }
     259             : 
     260             :   /// Writes the beginning of the file highlight for the file with the given
     261             :   /// [url].
     262           0 :   void _writeFileStart(Uri? url) {
     263           0 :     if (!_multipleFiles || url == null) {
     264           0 :       _writeSidebar(end: glyph.downEnd);
     265             :     } else {
     266           0 :       _writeSidebar(end: glyph.topLeftCorner);
     267           0 :       _colorize(() => _buffer.write('${glyph.horizontalLine * 2}>'),
     268             :           color: colors.blue);
     269           0 :       _buffer.write(' ${p.prettyUri(url)}');
     270             :     }
     271           0 :     _buffer.writeln();
     272             :   }
     273             : 
     274             :   /// Writes the post-sidebar highlight bars for [line] according to
     275             :   /// [highlightsByColumn].
     276             :   ///
     277             :   /// If [current] is passed, it's the highlight for which an indicator is being
     278             :   /// written. If it appears in [highlightsByColumn], a horizontal line is
     279             :   /// written from its column to the rightmost column.
     280           0 :   void _writeMultilineHighlights(
     281             :       _Line line, List<_Highlight?> highlightsByColumn,
     282             :       {_Highlight? current}) {
     283             :     // Whether we've written a sidebar indicator for opening a new span on this
     284             :     // line, and which color should be used for that indicator's rightward line.
     285             :     var openedOnThisLine = false;
     286             :     String? openedOnThisLineColor;
     287             : 
     288             :     final currentColor = current == null
     289             :         ? null
     290           0 :         : current.isPrimary
     291           0 :             ? _primaryColor
     292           0 :             : _secondaryColor;
     293             :     var foundCurrent = false;
     294           0 :     for (var highlight in highlightsByColumn) {
     295           0 :       final startLine = highlight?.span.start.line;
     296           0 :       final endLine = highlight?.span.end.line;
     297           0 :       if (current != null && highlight == current) {
     298             :         foundCurrent = true;
     299           0 :         assert(startLine == line.number || endLine == line.number);
     300           0 :         _colorize(() {
     301           0 :           _buffer.write(startLine == line.number
     302           0 :               ? glyph.topLeftCorner
     303           0 :               : glyph.bottomLeftCorner);
     304             :         }, color: currentColor);
     305             :       } else if (foundCurrent) {
     306           0 :         _colorize(() {
     307           0 :           _buffer.write(highlight == null ? glyph.horizontalLine : glyph.cross);
     308             :         }, color: currentColor);
     309             :       } else if (highlight == null) {
     310             :         if (openedOnThisLine) {
     311           0 :           _colorize(() => _buffer.write(glyph.horizontalLine),
     312             :               color: openedOnThisLineColor);
     313             :         } else {
     314           0 :           _buffer.write(' ');
     315             :         }
     316             :       } else {
     317           0 :         _colorize(() {
     318           0 :           final vertical = openedOnThisLine ? glyph.cross : glyph.verticalLine;
     319             :           if (current != null) {
     320           0 :             _buffer.write(vertical);
     321           0 :           } else if (startLine == line.number) {
     322           0 :             _colorize(() {
     323           0 :               _buffer
     324           0 :                   .write(glyph.glyphOrAscii(openedOnThisLine ? '┬' : '┌', '/'));
     325             :             }, color: openedOnThisLineColor);
     326             :             openedOnThisLine = true;
     327             :             openedOnThisLineColor ??=
     328           0 :                 highlight.isPrimary ? _primaryColor : _secondaryColor;
     329           0 :           } else if (endLine == line.number &&
     330           0 :               highlight.span.end.column == line.text.length) {
     331           0 :             _buffer.write(highlight.label == null
     332           0 :                 ? glyph.glyphOrAscii('└', '\\')
     333             :                 : vertical);
     334             :           } else {
     335           0 :             _colorize(() {
     336           0 :               _buffer.write(vertical);
     337             :             }, color: openedOnThisLineColor);
     338             :           }
     339           0 :         }, color: highlight.isPrimary ? _primaryColor : _secondaryColor);
     340             :       }
     341             :     }
     342             :   }
     343             : 
     344             :   // Writes [text], with text between [startColumn] and [endColumn] colorized in
     345             :   // the same way as [_colorize].
     346           0 :   void _writeHighlightedText(String text, int startColumn, int endColumn,
     347             :       {required String? color}) {
     348           0 :     _writeText(text.substring(0, startColumn));
     349           0 :     _colorize(() => _writeText(text.substring(startColumn, endColumn)),
     350             :         color: color);
     351           0 :     _writeText(text.substring(endColumn, text.length));
     352             :   }
     353             : 
     354             :   /// Writes an indicator for where [highlight] starts, ends, or both below
     355             :   /// [line].
     356             :   ///
     357             :   /// This may either add or remove [highlight] from [highlightsByColumn].
     358           0 :   void _writeIndicator(
     359             :       _Line line, _Highlight highlight, List<_Highlight?> highlightsByColumn) {
     360           0 :     final color = highlight.isPrimary ? _primaryColor : _secondaryColor;
     361           0 :     if (!isMultiline(highlight.span)) {
     362           0 :       _writeSidebar();
     363           0 :       _buffer.write(' ');
     364           0 :       _writeMultilineHighlights(line, highlightsByColumn, current: highlight);
     365           0 :       if (highlightsByColumn.isNotEmpty) _buffer.write(' ');
     366             : 
     367           0 :       _colorize(() {
     368           0 :         _writeUnderline(line, highlight.span,
     369           0 :             highlight.isPrimary ? '^' : glyph.horizontalLineBold);
     370           0 :         _writeLabel(highlight.label);
     371             :       }, color: color);
     372           0 :       _buffer.writeln();
     373           0 :     } else if (highlight.span.start.line == line.number) {
     374           0 :       if (highlightsByColumn.contains(highlight)) return;
     375           0 :       replaceFirstNull(highlightsByColumn, highlight);
     376             : 
     377           0 :       _writeSidebar();
     378           0 :       _buffer.write(' ');
     379           0 :       _writeMultilineHighlights(line, highlightsByColumn, current: highlight);
     380           0 :       _colorize(() => _writeArrow(line, highlight.span.start.column),
     381             :           color: color);
     382           0 :       _buffer.writeln();
     383           0 :     } else if (highlight.span.end.line == line.number) {
     384           0 :       final coversWholeLine = highlight.span.end.column == line.text.length;
     385           0 :       if (coversWholeLine && highlight.label == null) {
     386           0 :         replaceWithNull(highlightsByColumn, highlight);
     387             :         return;
     388             :       }
     389             : 
     390           0 :       _writeSidebar();
     391           0 :       _buffer.write(' ');
     392           0 :       _writeMultilineHighlights(line, highlightsByColumn, current: highlight);
     393             : 
     394           0 :       _colorize(() {
     395             :         if (coversWholeLine) {
     396           0 :           _buffer.write(glyph.horizontalLine * 3);
     397             :         } else {
     398           0 :           _writeArrow(line, math.max(highlight.span.end.column - 1, 0),
     399             :               beginning: false);
     400             :         }
     401           0 :         _writeLabel(highlight.label);
     402             :       }, color: color);
     403           0 :       _buffer.writeln();
     404           0 :       replaceWithNull(highlightsByColumn, highlight);
     405             :     }
     406             :   }
     407             : 
     408             :   /// Underlines the portion of [line] covered by [span] with repeated instances
     409             :   /// of [character].
     410           0 :   void _writeUnderline(_Line line, SourceSpan span, String character) {
     411           0 :     assert(!isMultiline(span));
     412           0 :     assert(line.text.contains(span.text));
     413             : 
     414           0 :     var startColumn = span.start.column;
     415           0 :     var endColumn = span.end.column;
     416             : 
     417             :     // Adjust the start and end columns to account for any tabs that were
     418             :     // converted to spaces.
     419           0 :     final tabsBefore = _countTabs(line.text.substring(0, startColumn));
     420           0 :     final tabsInside = _countTabs(line.text.substring(startColumn, endColumn));
     421           0 :     startColumn += tabsBefore * (_spacesPerTab - 1);
     422           0 :     endColumn += (tabsBefore + tabsInside) * (_spacesPerTab - 1);
     423             : 
     424           0 :     _buffer
     425           0 :       ..write(' ' * startColumn)
     426           0 :       ..write(character * math.max(endColumn - startColumn, 1));
     427             :   }
     428             : 
     429             :   /// Write an arrow pointing to column [column] in [line].
     430             :   ///
     431             :   /// If the arrow points to a tab character, this will point to the beginning
     432             :   /// of the tab if [beginning] is `true` and the end if it's `false`.
     433           0 :   void _writeArrow(_Line line, int column, {bool beginning = true}) {
     434             :     final tabs =
     435           0 :         _countTabs(line.text.substring(0, column + (beginning ? 0 : 1)));
     436           0 :     _buffer
     437           0 :       ..write(glyph.horizontalLine * (1 + column + tabs * (_spacesPerTab - 1)))
     438           0 :       ..write('^');
     439             :   }
     440             : 
     441             :   /// Writes a space followed by [label] if [label] isn't `null`.
     442           0 :   void _writeLabel(String? label) {
     443           0 :     if (label != null) _buffer.write(' $label');
     444             :   }
     445             : 
     446             :   /// Writes a snippet from the source text, converting hard tab characters into
     447             :   /// plain indentation.
     448           0 :   void _writeText(String text) {
     449           0 :     for (var char in text.codeUnits) {
     450           0 :       if (char == $tab) {
     451           0 :         _buffer.write(' ' * _spacesPerTab);
     452             :       } else {
     453           0 :         _buffer.writeCharCode(char);
     454             :       }
     455             :     }
     456             :   }
     457             : 
     458             :   // Writes a sidebar to [buffer] that includes [line] as the line number if
     459             :   // given and writes [end] at the end (defaults to [glyphs.verticalLine]).
     460             :   //
     461             :   // If [text] is given, it's used in place of the line number. It can't be
     462             :   // passed at the same time as [line].
     463           0 :   void _writeSidebar({int? line, String? text, String? end}) {
     464           0 :     assert(line == null || text == null);
     465             : 
     466             :     // Add 1 to line to convert from computer-friendly 0-indexed line numbers to
     467             :     // human-friendly 1-indexed line numbers.
     468           0 :     if (line != null) text = (line + 1).toString();
     469           0 :     _colorize(() {
     470           0 :       _buffer
     471           0 :         ..write((text ?? '').padRight(_paddingBeforeSidebar))
     472           0 :         ..write(end ?? glyph.verticalLine);
     473             :     }, color: colors.blue);
     474             :   }
     475             : 
     476             :   /// Returns the number of hard tabs in [text].
     477           0 :   int _countTabs(String text) {
     478             :     var count = 0;
     479           0 :     for (var char in text.codeUnits) {
     480           0 :       if (char == $tab) count++;
     481             :     }
     482             :     return count;
     483             :   }
     484             : 
     485             :   /// Returns whether [text] contains only space or tab characters.
     486           0 :   bool _isOnlyWhitespace(String text) {
     487           0 :     for (var char in text.codeUnits) {
     488           0 :       if (char != $space && char != $tab) return false;
     489             :     }
     490             :     return true;
     491             :   }
     492             : 
     493             :   /// Colors all text written to [_buffer] during [callback], if colorization is
     494             :   /// enabled and [color] is not `null`.
     495           0 :   void _colorize(void Function() callback, {required String? color}) {
     496           0 :     if (_primaryColor != null && color != null) _buffer.write(color);
     497             :     callback();
     498           0 :     if (_primaryColor != null && color != null) _buffer.write(colors.none);
     499             :   }
     500             : }
     501             : 
     502             : /// Information about how to highlight a single section of a source file.
     503             : class _Highlight {
     504             :   /// The section of the source file to highlight.
     505             :   ///
     506             :   /// This is normalized to make it easier for [Highlighter] to work with.
     507             :   final SourceSpanWithContext span;
     508             : 
     509             :   /// Whether this is the primary span in the highlight.
     510             :   ///
     511             :   /// The primary span is highlighted with a different character and colored
     512             :   /// differently than non-primary spans.
     513             :   final bool isPrimary;
     514             : 
     515             :   /// The label to include inline when highlighting [span].
     516             :   ///
     517             :   /// This helps distinguish clarify what each highlight means when multiple are
     518             :   /// used in the same message.
     519             :   final String? label;
     520             : 
     521           0 :   _Highlight(SourceSpan span, {this.label, bool primary = false})
     522           0 :       : span = (() {
     523           0 :           var newSpan = _normalizeContext(span);
     524           0 :           newSpan = _normalizeNewlines(newSpan);
     525           0 :           newSpan = _normalizeTrailingNewline(newSpan);
     526           0 :           return _normalizeEndOfLine(newSpan);
     527             :         })(),
     528             :         isPrimary = primary;
     529             : 
     530             :   /// Normalizes [span] to ensure that it's a [SourceSpanWithContext] whose
     531             :   /// context actually contains its text at the expected column.
     532             :   ///
     533             :   /// If it's not already a [SourceSpanWithContext], adjust the start and end
     534             :   /// locations' line and column fields so that the highlighter can assume they
     535             :   /// match up with the context.
     536           0 :   static SourceSpanWithContext _normalizeContext(SourceSpan span) =>
     537           0 :       span is SourceSpanWithContext &&
     538           0 :               findLineStart(span.context, span.text, span.start.column) != null
     539             :           ? span
     540           0 :           : SourceSpanWithContext(
     541           0 :               SourceLocation(span.start.offset,
     542           0 :                   sourceUrl: span.sourceUrl, line: 0, column: 0),
     543           0 :               SourceLocation(span.end.offset,
     544           0 :                   sourceUrl: span.sourceUrl,
     545           0 :                   line: countCodeUnits(span.text, $lf),
     546           0 :                   column: _lastLineLength(span.text)),
     547           0 :               span.text,
     548           0 :               span.text);
     549             : 
     550             :   /// Normalizes [span] to replace Windows-style newlines with Unix-style
     551             :   /// newlines.
     552           0 :   static SourceSpanWithContext _normalizeNewlines(SourceSpanWithContext span) {
     553           0 :     final text = span.text;
     554           0 :     if (!text.contains('\r\n')) return span;
     555             : 
     556           0 :     var endOffset = span.end.offset;
     557           0 :     for (var i = 0; i < text.length - 1; i++) {
     558           0 :       if (text.codeUnitAt(i) == $cr && text.codeUnitAt(i + 1) == $lf) {
     559           0 :         endOffset--;
     560             :       }
     561             :     }
     562             : 
     563           0 :     return SourceSpanWithContext(
     564           0 :         span.start,
     565           0 :         SourceLocation(endOffset,
     566           0 :             sourceUrl: span.sourceUrl,
     567           0 :             line: span.end.line,
     568           0 :             column: span.end.column),
     569           0 :         text.replaceAll('\r\n', '\n'),
     570           0 :         span.context.replaceAll('\r\n', '\n'));
     571             :   }
     572             : 
     573             :   /// Normalizes [span] to remove a trailing newline from `span.context`.
     574             :   ///
     575             :   /// If necessary, also adjust `span.end` so that it doesn't point past where
     576             :   /// the trailing newline used to be.
     577           0 :   static SourceSpanWithContext _normalizeTrailingNewline(
     578             :       SourceSpanWithContext span) {
     579           0 :     if (!span.context.endsWith('\n')) return span;
     580             : 
     581             :     // If there's a full blank line on the end of [span.context], it's probably
     582             :     // significant, so we shouldn't trim it.
     583           0 :     if (span.text.endsWith('\n\n')) return span;
     584             : 
     585           0 :     final context = span.context.substring(0, span.context.length - 1);
     586           0 :     var text = span.text;
     587           0 :     var start = span.start;
     588           0 :     var end = span.end;
     589           0 :     if (span.text.endsWith('\n') && _isTextAtEndOfContext(span)) {
     590           0 :       text = span.text.substring(0, span.text.length - 1);
     591           0 :       if (text.isEmpty) {
     592             :         end = start;
     593             :       } else {
     594           0 :         end = SourceLocation(span.end.offset - 1,
     595           0 :             sourceUrl: span.sourceUrl,
     596           0 :             line: span.end.line - 1,
     597           0 :             column: _lastLineLength(context));
     598           0 :         start = span.start.offset == span.end.offset ? end : span.start;
     599             :       }
     600             :     }
     601           0 :     return SourceSpanWithContext(start, end, text, context);
     602             :   }
     603             : 
     604             :   /// Normalizes [span] so that the end location is at the end of a line rather
     605             :   /// than at the beginning of the next line.
     606           0 :   static SourceSpanWithContext _normalizeEndOfLine(SourceSpanWithContext span) {
     607           0 :     if (span.end.column != 0) return span;
     608           0 :     if (span.end.line == span.start.line) return span;
     609             : 
     610           0 :     final text = span.text.substring(0, span.text.length - 1);
     611             : 
     612           0 :     return SourceSpanWithContext(
     613           0 :         span.start,
     614           0 :         SourceLocation(span.end.offset - 1,
     615           0 :             sourceUrl: span.sourceUrl,
     616           0 :             line: span.end.line - 1,
     617           0 :             column: text.length - text.lastIndexOf('\n') - 1),
     618             :         text,
     619             :         // If the context also ends with a newline, it's possible that we don't
     620             :         // have the full context for that line, so we shouldn't print it at all.
     621           0 :         span.context.endsWith('\n')
     622           0 :             ? span.context.substring(0, span.context.length - 1)
     623           0 :             : span.context);
     624             :   }
     625             : 
     626             :   /// Returns the length of the last line in [text], whether or not it ends in a
     627             :   /// newline.
     628           0 :   static int _lastLineLength(String text) {
     629           0 :     if (text.isEmpty) {
     630             :       return 0;
     631           0 :     } else if (text.codeUnitAt(text.length - 1) == $lf) {
     632           0 :       return text.length == 1
     633             :           ? 0
     634           0 :           : text.length - text.lastIndexOf('\n', text.length - 2) - 1;
     635             :     } else {
     636           0 :       return text.length - text.lastIndexOf('\n') - 1;
     637             :     }
     638             :   }
     639             : 
     640             :   /// Returns whether [span]'s text runs all the way to the end of its context.
     641           0 :   static bool _isTextAtEndOfContext(SourceSpanWithContext span) =>
     642           0 :       findLineStart(span.context, span.text, span.start.column)! +
     643           0 :           span.start.column +
     644           0 :           span.length ==
     645           0 :       span.context.length;
     646             : 
     647           0 :   @override
     648             :   String toString() {
     649           0 :     final buffer = StringBuffer();
     650           0 :     if (isPrimary) buffer.write('primary ');
     651           0 :     buffer.write('${span.start.line}:${span.start.column}-'
     652           0 :         '${span.end.line}:${span.end.column}');
     653           0 :     if (label != null) buffer.write(' ($label)');
     654           0 :     return buffer.toString();
     655             :   }
     656             : }
     657             : 
     658             : /// A single line of the source file being highlighted.
     659             : class _Line {
     660             :   /// The text of the line, not including the trailing newline.
     661             :   final String text;
     662             : 
     663             :   /// The 0-based line number in the source file.
     664             :   final int number;
     665             : 
     666             :   /// The URL of the source file in which this line appears.
     667             :   final Uri? url;
     668             : 
     669             :   /// All highlights that cover any portion of this line, in source span order.
     670             :   ///
     671             :   /// This is populated after the initial line is created.
     672             :   final highlights = <_Highlight>[];
     673             : 
     674           0 :   _Line(this.text, this.number, this.url);
     675             : 
     676           0 :   @override
     677           0 :   String toString() => '$number: "$text" (${highlights.join(', ')})';
     678             : }

Generated by: LCOV version 1.14