diff --git a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_view.dart b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_view.dart index ad800d5dbf1..046adec0f75 100644 --- a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_view.dart +++ b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_view.dart @@ -308,7 +308,7 @@ class _NoEditablePropertiesMessage extends StatelessWidget { ' has no editable widget properties.\n\nThe Flutter Property Editor currently supports editing properties of type ', style: theme.regularTextStyle, ), - TextSpan(text: 'string', style: fixedFontStyle), + TextSpan(text: 'String', style: fixedFontStyle), const TextSpan(text: ', '), TextSpan(text: 'int', style: fixedFontStyle), const TextSpan(text: ', '), @@ -341,31 +341,34 @@ class _WidgetNameAndDocumentation extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: denseSpacing), - child: Text( - name, - style: Theme.of(context).fixedFontStyle.copyWith( - fontWeight: FontWeight.bold, - fontSize: defaultFontSize + 1, + return SelectionArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(bottom: denseSpacing), + child: Text( + name, + style: Theme.of(context).fixedFontStyle.copyWith( + fontWeight: FontWeight.bold, + fontSize: defaultFontSize + 1, + ), ), ), - ), - Row( - children: [ - Expanded( - child: _ExpandableWidgetDocumentation( - documentation: - documentation ?? 'Creates ${addIndefiniteArticle(name)}.', + Row( + children: [ + Expanded( + child: _ExpandableWidgetDocumentation( + documentation: + documentation ?? 'Creates ${addIndefiniteArticle(name)}.', + ), ), - ), - ], - ), - const PaddedDivider.noPadding(), - ], + ], + ), + const PaddedDivider.noPadding(), + ], + ), ); } } @@ -410,8 +413,7 @@ class _ExpandableWidgetDocumentationState final paragraphs = widget.documentation.split('\n'); if (paragraphs.length == 1) { - return convertDartDocToRichText( - widget.documentation, + return DartDocConverter(widget.documentation).toText( regularFontStyle: regularFontStyle, fixedFontStyle: fixedFontStyle, ); @@ -426,16 +428,14 @@ class _ExpandableWidgetDocumentationState // or collapses the text block. Because the Dart doc is never very // large, this is not an expensive operation. However, we could // consider caching the result if this needs to be optimized. - convertDartDocToRichText( - firstParagraph, + DartDocConverter(firstParagraph).toText( regularFontStyle: regularFontStyle, fixedFontStyle: fixedFontStyle, ), if (_isExpanded) FadeTransition( opacity: _expandAnimation, - child: convertDartDocToRichText( - otherParagraphs.join('\n'), + child: DartDocConverter(otherParagraphs.join('\n')).toText( regularFontStyle: regularFontStyle, fixedFontStyle: fixedFontStyle, ), diff --git a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/utils/utils.dart b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/utils/utils.dart index fa3f1d7e3a7..02c9505dd9a 100644 --- a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/utils/utils.dart +++ b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/utils/utils.dart @@ -5,77 +5,94 @@ import 'package:flutter/widgets.dart'; import '_utils_desktop.dart' if (dart.library.js_interop) '_utils_web.dart'; -/// Converts a [dartDocText] String into a [RichText] widget. -/// -/// Removes any brackets and backticks and displays the text inside them as -/// fixed font. -RichText convertDartDocToRichText( - String dartDocText, { - required TextStyle regularFontStyle, - required TextStyle fixedFontStyle, -}) { - final children = []; - int currentIndex = 0; +/// Converts a [dartDocText] String into a [Text] widget. +class DartDocConverter { + DartDocConverter(this.dartDocText); - while (currentIndex < dartDocText.length) { - final openBracketIndex = dartDocText.indexOf('[', currentIndex); - final openBacktickIndex = dartDocText.indexOf('`', currentIndex); + final String dartDocText; - int nextSpecialCharIndex = -1; - bool isLink = false; + /// Converts the [dartDocText] String into a [Text] widget. + /// + /// Removes any brackets and backticks and displays the text inside them with + /// [fixedFontStyle]. All other text uses [regularFontStyle]. + Text toText({ + required TextStyle regularFontStyle, + required TextStyle fixedFontStyle, + }) { + final children = toTextSpans( + regularFontStyle: regularFontStyle, + fixedFontStyle: fixedFontStyle, + ); + return Text.rich(TextSpan(children: children)); + } - if (openBracketIndex != -1 && - (openBacktickIndex == -1 || openBracketIndex < openBacktickIndex)) { - nextSpecialCharIndex = openBracketIndex; - isLink = true; - } else if (openBacktickIndex != -1 && - (openBracketIndex == -1 || openBacktickIndex < openBracketIndex)) { - nextSpecialCharIndex = openBacktickIndex; - } + @visibleForTesting + List toTextSpans({ + required TextStyle regularFontStyle, + required TextStyle fixedFontStyle, + }) { + final children = []; + int currentIndex = 0; - if (nextSpecialCharIndex == -1) { - // No more special characters, add the remaining text. - children.add( - TextSpan( - text: dartDocText.substring(currentIndex), - style: regularFontStyle, - ), - ); - break; - } + while (currentIndex < dartDocText.length) { + final openBracketIndex = dartDocText.indexOf('[', currentIndex); + final openBacktickIndex = dartDocText.indexOf('`', currentIndex); - // Add text before the special character. - children.add( - TextSpan( - text: dartDocText.substring(currentIndex, nextSpecialCharIndex), - style: regularFontStyle, - ), - ); + int nextSpecialCharIndex = -1; + bool isLink = false; - final closeIndex = dartDocText.indexOf( - isLink ? ']' : '`', - isLink ? nextSpecialCharIndex : nextSpecialCharIndex + 1, - ); - if (closeIndex == -1) { - // Treat unmatched brackets/backticks as regular text. + if (openBracketIndex != -1 && + (openBacktickIndex == -1 || openBracketIndex < openBacktickIndex)) { + nextSpecialCharIndex = openBracketIndex; + isLink = true; + } else if (openBacktickIndex != -1 && + (openBracketIndex == -1 || openBacktickIndex < openBracketIndex)) { + nextSpecialCharIndex = openBacktickIndex; + } + + if (nextSpecialCharIndex == -1) { + // No more special characters, add the remaining text. + children.add( + TextSpan( + text: dartDocText.substring(currentIndex), + style: regularFontStyle, + ), + ); + break; + } + + // Add text before the special character. children.add( TextSpan( - text: dartDocText.substring(nextSpecialCharIndex), + text: dartDocText.substring(currentIndex, nextSpecialCharIndex), style: regularFontStyle, ), ); - currentIndex = dartDocText.length; // Effectively break the loop. - } else { - final content = dartDocText.substring( - nextSpecialCharIndex + 1, - closeIndex, + + final closeIndex = dartDocText.indexOf( + isLink ? ']' : '`', + isLink ? nextSpecialCharIndex : nextSpecialCharIndex + 1, ); - children.add(TextSpan(text: content, style: fixedFontStyle)); - currentIndex = closeIndex + 1; + if (closeIndex == -1) { + // Treat unmatched brackets/backticks as regular text. + children.add( + TextSpan( + text: dartDocText.substring(nextSpecialCharIndex), + style: regularFontStyle, + ), + ); + currentIndex = dartDocText.length; // Effectively break the loop. + } else { + final content = dartDocText.substring( + nextSpecialCharIndex + 1, + closeIndex, + ); + children.add(TextSpan(text: content, style: fixedFontStyle)); + currentIndex = closeIndex + 1; + } } + return children; } - - return RichText(text: TextSpan(children: children)); } /// Workaround to force reload the Property Editor when it disconnects. diff --git a/packages/devtools_app/test/standalone_ui/ide_shared/property_editor/utils_test.dart b/packages/devtools_app/test/standalone_ui/ide_shared/property_editor/utils_test.dart index 2d84742e34f..ee547852668 100644 --- a/packages/devtools_app/test/standalone_ui/ide_shared/property_editor/utils_test.dart +++ b/packages/devtools_app/test/standalone_ui/ide_shared/property_editor/utils_test.dart @@ -11,35 +11,38 @@ void main() { const regularFontStyle = TextStyle(color: Colors.black); const fixedFontStyle = TextStyle(color: Colors.blue); - testWidgets('convertDartDocToRichText handles plain text', ( + testWidgets('DartDocConverter handles plain text', ( WidgetTester tester, ) async { - final richText = convertDartDocToRichText( - 'This is a Dart doc comment.', + final converter = DartDocConverter('This is a Dart doc comment.'); + + final text = converter.toText( regularFontStyle: regularFontStyle, fixedFontStyle: fixedFontStyle, ); - expect(richText.text.toPlainText(), equals('This is a Dart doc comment.')); - expect( - _hasStyle(_children(richText).first, style: regularFontStyle), - isTrue, + final children = converter.toTextSpans( + regularFontStyle: regularFontStyle, + fixedFontStyle: fixedFontStyle, ); + expect(text.textSpan?.toPlainText(), equals('This is a Dart doc comment.')); + expect(_hasStyle(children.first, style: regularFontStyle), isTrue); }); - testWidgets('convertDartDocToRichText handles links', ( - WidgetTester tester, - ) async { - final richText = convertDartDocToRichText( - 'This is a [link].', + testWidgets('DartDocConverter handles links', (WidgetTester tester) async { + final converter = DartDocConverter('This is a [link].'); + final text = converter.toText( + regularFontStyle: regularFontStyle, + fixedFontStyle: fixedFontStyle, + ); + final children = converter.toTextSpans( regularFontStyle: regularFontStyle, fixedFontStyle: fixedFontStyle, ); - final children = _children(richText); final firstChild = children.first; final secondChild = children.second; final thirdChild = children.third; - expect(richText.text.toPlainText(), equals('This is a link.')); + expect(text.textSpan?.toPlainText(), equals('This is a link.')); expect(firstChild.toPlainText(), equals('This is a ')); expect(_hasStyle(firstChild, style: regularFontStyle), isTrue); expect(secondChild.toPlainText(), equals('link')); @@ -48,20 +51,23 @@ void main() { expect(_hasStyle(thirdChild, style: regularFontStyle), isTrue); }); - testWidgets('convertDartDocToRichText handles code blocks', ( + testWidgets('DartDocConverter handles code blocks', ( WidgetTester tester, ) async { - final richText = convertDartDocToRichText( - 'This is `code`.', + final converter = DartDocConverter('This is `code`.'); + final text = converter.toText( + regularFontStyle: regularFontStyle, + fixedFontStyle: fixedFontStyle, + ); + final children = converter.toTextSpans( regularFontStyle: regularFontStyle, fixedFontStyle: fixedFontStyle, ); - final children = _children(richText); final firstChild = children.first; final secondChild = children.second; final thirdChild = children.third; - expect(richText.text.toPlainText(), equals('This is code.')); + expect(text.textSpan?.toPlainText(), equals('This is code.')); expect(firstChild.toPlainText(), equals('This is ')); expect(_hasStyle(firstChild, style: regularFontStyle), isTrue); expect(secondChild.toPlainText(), equals('code')); @@ -70,15 +76,18 @@ void main() { expect(_hasStyle(thirdChild, style: regularFontStyle), isTrue); }); - testWidgets('convertDartDocToRichText handles mixed content', ( + testWidgets('DartDocConverter handles mixed content', ( WidgetTester tester, ) async { - final richText = convertDartDocToRichText( - 'This is [a link] and `some code`.', + final converter = DartDocConverter('This is [a link] and `some code`.'); + final text = converter.toText( + regularFontStyle: regularFontStyle, + fixedFontStyle: fixedFontStyle, + ); + final children = converter.toTextSpans( regularFontStyle: regularFontStyle, fixedFontStyle: fixedFontStyle, ); - final children = _children(richText); final firstChild = children.first; final secondChild = children.second; final thirdChild = children.third; @@ -86,7 +95,7 @@ void main() { final fifthChild = children.fifth; expect( - richText.text.toPlainText(), + text.textSpan?.toPlainText(), equals('This is a link and some code.'), ); expect(firstChild.toPlainText(), equals('This is ')); @@ -101,63 +110,62 @@ void main() { expect(_hasStyle(fifthChild, style: regularFontStyle), isTrue); }); - testWidgets('convertDartDocToRichText handles unmatched brackets', ( + testWidgets('DartDocConverter handles unmatched brackets', ( WidgetTester tester, ) async { - final richText = convertDartDocToRichText( - 'Unmatched [bracket.', + final converter = DartDocConverter('Unmatched [bracket.'); + final text = converter.toText( + regularFontStyle: regularFontStyle, + fixedFontStyle: fixedFontStyle, + ); + final children = converter.toTextSpans( regularFontStyle: regularFontStyle, fixedFontStyle: fixedFontStyle, ); - final children = _children(richText); final firstChild = children.first; final secondChild = children.second; - expect(richText.text.toPlainText(), equals('Unmatched [bracket.')); + expect(text.textSpan?.toPlainText(), equals('Unmatched [bracket.')); expect(firstChild.toPlainText(), equals('Unmatched ')); expect(_hasStyle(firstChild, style: regularFontStyle), isTrue); expect(secondChild.toPlainText(), equals('[bracket.')); expect(_hasStyle(secondChild, style: regularFontStyle), isTrue); }); - testWidgets('convertDartDocToRichText handles unmatched backticks', ( + testWidgets('DartDocConverter handles unmatched backticks', ( WidgetTester tester, ) async { - final richText = convertDartDocToRichText( - 'Unmatched `backtick.', + final converter = DartDocConverter('Unmatched `backtick.'); + final text = converter.toText( + regularFontStyle: regularFontStyle, + fixedFontStyle: fixedFontStyle, + ); + final children = converter.toTextSpans( regularFontStyle: regularFontStyle, fixedFontStyle: fixedFontStyle, ); - final children = _children(richText); final firstChild = children.first; final secondChild = children.second; - expect(richText.text.toPlainText(), equals('Unmatched `backtick.')); + expect(text.textSpan?.toPlainText(), equals('Unmatched `backtick.')); expect(firstChild.toPlainText(), equals('Unmatched ')); expect(_hasStyle(firstChild, style: regularFontStyle), isTrue); expect(secondChild.toPlainText(), equals('`backtick.')); expect(_hasStyle(secondChild, style: regularFontStyle), isTrue); }); - testWidgets('convertDartDocToRichText handles empty strings', ( + testWidgets('DartDocConverter handles empty strings', ( WidgetTester tester, ) async { - final richText = convertDartDocToRichText( - '', + final converter = DartDocConverter(''); + final text = converter.toText( regularFontStyle: regularFontStyle, fixedFontStyle: fixedFontStyle, ); - expect(richText.text.toPlainText(), equals('')); + expect(text.textSpan?.toPlainText(), equals('')); }); } -List _children(RichText richText) => - (_asTextSpan(richText.text).children ?? []) - .map((childSpan) => _asTextSpan(childSpan)) - .toList(); - bool _hasStyle(TextSpan span, {required TextStyle style}) => span.style!.color == style.color; - -TextSpan _asTextSpan(InlineSpan span) => span as TextSpan;