buildTextSpan method

  1. @override
TextSpan buildTextSpan({
  1. required BuildContext context,
  2. TextStyle? style,
  3. 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,
  bool? withComposing,
}) {
  TextStyle baseStyle = TextStyle(
    color: editorTheme['root']?.color,
    height: 1.5,
  );

  if (textStyle != null) {
    baseStyle = baseStyle.merge(textStyle);
  }

  final int cursorPosition = selection.baseOffset;
  int? bracket1, bracket2;

  if (Shared().aiResponse != null &&
      text.isNotEmpty &&
      Shared().aiResponse!.isNotEmpty) {
    final String textBeforeCursor = text.substring(0, cursorPosition);
    final String textAfterCursor = text.substring(cursorPosition);

    final bool atLineEnd =
        textAfterCursor.isEmpty ||
        textAfterCursor.startsWith('\n') ||
        textAfterCursor.trim().isEmpty;

    if (!atLineEnd) {
      return TextSpan(text: text, style: baseStyle);
    }
    final String lastTypedChar = textBeforeCursor.isNotEmpty
        ? textBeforeCursor[textBeforeCursor.length - 1].replaceAll("\n", '')
        : '';
    final List<Node>? beforeCursorNodes = highlight
        .parse(textBeforeCursor, language: _langId)
        .nodes;

    if (Shared().lastCursorPosition != null &&
        cursorPosition != Shared().lastCursorPosition) {
      final movedCursor =
          (cursorPosition != Shared().lastCursorPosition! + 1);
      final mismatch =
          lastTypedChar.trim().isNotEmpty &&
          (Shared().aiResponse == null ||
              Shared().aiResponse!.isEmpty ||
              Shared().aiResponse![0] != lastTypedChar);
      if (movedCursor || mismatch) {
        Shared().aiResponse = null;
      }
    }

    if (Shared().aiResponse != null &&
        Shared().aiResponse!.isNotEmpty &&
        Shared().aiResponse![0] == lastTypedChar) {
      Shared().aiResponse = Shared().aiResponse!.substring(1);
    }

    Shared().lastCursorPosition = cursorPosition;

    TextSpan aiOverlay = TextSpan(
      text: Shared().aiResponse,
      style:
          Shared().aiOverlayStyle ??
          TextStyle(color: Colors.grey, fontStyle: FontStyle.italic),
    );
    final List<Node>? afterCursorNodes = highlight
        .parse(textAfterCursor, language: _langId)
        .nodes;

    if (beforeCursorNodes != null) {
      if (cursorPosition != selection.baseOffset) {
        Shared().aiResponse = null;
      }
      return TextSpan(
        style: baseStyle,
        children: [
          ..._convert(beforeCursorNodes),
          aiOverlay,
          ..._convert(afterCursorNodes ?? <Node>[]),
        ],
      );
    }
  }

  final List<String> lines = text.isNotEmpty ? text.split("\n") : [];

  if (cursorPosition >= 0 && cursorPosition <= text.length) {
    final String? before = cursorPosition > 0
        ? text[cursorPosition - 1]
        : null;
    final String? after = cursorPosition < text.length
        ? text[cursorPosition]
        : null;
    final int? pos = (before != null && '{}[]()'.contains(before))
        ? cursorPosition - 1
        : (after != null && '{}[]()'.contains(after))
        ? cursorPosition
        : null;

    if (pos != null) {
      final match = _findMatchingBracket(text, pos);
      if (match != null) {
        bracket1 = pos;
        bracket2 = match;
      }
    }
  }

  final List<FoldRange> foldedRanges = Shared().lineStates.value
      .where((line) => line.foldRange?.isFolded == true)
      .map((line) => line.foldRange!)
      .toList();

  foldedRanges.sort((a, b) => a.startLine.compareTo(b.startLine));
  final filteredFolds = <FoldRange>[];
  for (final FoldRange fold in foldedRanges) {
    bool isNested = filteredFolds.any(
      (parent) =>
          fold.startLine >= parent.startLine &&
          fold.endLine <= parent.endLine,
    );
    if (!isNested) filteredFolds.add(fold);
  }
  filteredFolds.sort((a, b) => b.startLine.compareTo(a.startLine));

  for (final fold in filteredFolds) {
    int start = fold.startLine - 1;
    int end = fold.endLine;
    if (start >= 0 && end <= lines.length && start < end) {
      lines[start] =
          "${lines[start]}...${'\u200D' * ((lines.sublist(start + 1, end).join('\n').length) - 2)}";
      lines.removeRange(start + 1, end);
    }
  }

  final newText = lines.join('\n');

  final List<Node>? nodes = highlight.parse(newText, language: _langId).nodes;
  final Set<int> unmatchedBrackets = _findUnmatchedBrackets(text);
  if (nodes != null && editorTheme.isNotEmpty) {
    return TextSpan(
      style: baseStyle,
      children: _convert(nodes, 0, bracket1, bracket2, unmatchedBrackets),
    );
  } else {
    return TextSpan(text: newText, style: textStyle);
  }
}