buildTextSpan method

  1. @override
TextSpan buildTextSpan({
  1. required BuildContext context,
  2. TextStyle? style,
  3. required bool withComposing,
})
override

Builds TextSpan from current editing value.

By default makes text in composing range appear as underlined. Descendants can override this method to customize appearance of text.

Implementation

@override
TextSpan buildTextSpan({
  required BuildContext context,
  TextStyle? style,
  required bool withComposing,
}) {
  final theme = Theme.of(context);
  final base = (style ?? const TextStyle()).copyWith(
    fontFamily: 'monospace',
    fontSize: style?.fontSize ?? 12,
    height: style?.height ?? 1.35,
    color: style?.color ?? theme.colorScheme.onSurface,
  );

  final keyStyle = base.copyWith(
    color: theme.colorScheme.primary,
    fontWeight: FontWeight.w600,
  );
  final stringStyle = base.copyWith(
    color: theme.brightness == Brightness.dark
        ? const Color(0xFFCE9178)
        : const Color(0xFFA31515),
  );
  final numberStyle = base.copyWith(
    color: theme.brightness == Brightness.dark
        ? const Color(0xFFB5CEA8)
        : const Color(0xFF098658),
  );
  final keywordStyle = base.copyWith(
    color: theme.brightness == Brightness.dark
        ? const Color(0xFF569CD6)
        : const Color(0xFF0000FF),
    fontWeight: FontWeight.w600,
  );
  final punctStyle = base.copyWith(
    color: theme.colorScheme.onSurface.withValues(alpha: 0.55),
  );

  final text = value.text;
  if (text.isEmpty) {
    return TextSpan(style: base, text: '');
  }

  final issue = findJsonParseIssue(text);
  final match = findMatchingBracket(text, value.selection.baseOffset);
  final errorColor = theme.colorScheme.error;
  final matchColor = theme.colorScheme.primary.withValues(alpha: 0.22);

  final spans = <InlineSpan>[];
  var start = 0;

  void emit(int from, int to, TextStyle tokenStyle) {
    if (from >= to) return;
    var cursor = from;
    while (cursor < to) {
      var next = to;
      TextStyle styleForRun = tokenStyle;

      if (issue != null) {
        final errStart = issue.offset.clamp(0, text.length);
        final errEnd = issue.end.clamp(0, text.length);
        if (cursor < errEnd && next > errStart) {
          if (cursor < errStart) {
            next = errStart;
          } else {
            next = math.min(next, errEnd);
            styleForRun = tokenStyle.copyWith(
              decoration: TextDecoration.underline,
              decorationColor: errorColor,
              decorationStyle: TextDecorationStyle.wavy,
              decorationThickness: 1.6,
              color: errorColor,
            );
          }
        }
      }

      if (match != null) {
        for (final index in [match.open, match.close]) {
          if (cursor <= index && index < next) {
            if (cursor < index) {
              next = index;
            } else {
              next = index + 1;
              styleForRun = styleForRun.copyWith(
                backgroundColor: matchColor,
                fontWeight: FontWeight.w800,
                color: theme.colorScheme.primary,
              );
            }
            break;
          }
        }
      }

      spans.add(
        TextSpan(
          text: text.substring(cursor, next),
          style: styleForRun,
        ),
      );
      cursor = next;
    }
  }

  for (final tokenMatch in _tokenPattern.allMatches(text)) {
    if (tokenMatch.start > start) {
      emit(start, tokenMatch.start, base);
    }
    final token = tokenMatch.group(0)!;
    final TextStyle tokenStyle;
    if (token.endsWith(':')) {
      tokenStyle = keyStyle;
    } else if (token.startsWith('"')) {
      tokenStyle = stringStyle;
    } else if (token == 'true' || token == 'false' || token == 'null') {
      tokenStyle = keywordStyle;
    } else if (RegExp(r'^-?\d').hasMatch(token)) {
      tokenStyle = numberStyle;
    } else {
      tokenStyle = punctStyle;
    }
    emit(tokenMatch.start, tokenMatch.end, tokenStyle);
    start = tokenMatch.end;
  }
  if (start < text.length) {
    emit(start, text.length, base);
  }

  return TextSpan(style: base, children: spans);
}