Repository: superlistapp/super_editor

Issue:
[SuperEditor] - Fix: Pass inline widget builders into `HintTextComponent`
It looks like `HintTextComponent` is never provided with inline widget builders, and therefore breaks any use-case that wants to build inline widgets in a paragraph that includes a hint.

Pipe through the inline widget builders from the view model.



Fix patch:
diff --git a/super_editor/lib/src/default_editor/paragraph.dart b/super_editor/lib/src/default_editor/paragraph.dart
index 1d133ce11..572c185a0 100644
--- a/super_editor/lib/src/default_editor/paragraph.dart
+++ b/super_editor/lib/src/default_editor/paragraph.dart
@@ -331,6 +331,7 @@ class HintComponentBuilder extends ParagraphComponentBuilder {
     return TextWithHintComponent(
       key: componentContext.componentKey,
       text: componentViewModel.text,
+      inlineWidgetBuilders: componentViewModel.inlineWidgetBuilders,
       textStyleBuilder: componentViewModel.textStyleBuilder,
       hintText: AttributedText(componentViewModel.hintText),
       hintStyleBuilder: (attributions) => hintStyleBuilder(componentContext.context),
@@ -422,6 +423,7 @@ class HintComponentViewModel extends SingleColumnLayoutComponentViewModel with T
         createdAt: createdAt,
         padding: padding,
         text: text.copy(),
+        inlineWidgetBuilders: inlineWidgetBuilders,
         textStyleBuilder: textStyleBuilder,
         opacity: opacity,
         selectionColor: selectionColor,
diff --git a/super_editor/lib/src/default_editor/text.dart b/super_editor/lib/src/default_editor/text.dart
index 5d135f16f..cf46e99e0 100644
--- a/super_editor/lib/src/default_editor/text.dart
+++ b/super_editor/lib/src/default_editor/text.dart
@@ -691,6 +691,7 @@ class TextWithHintComponent extends StatefulWidget {
   const TextWithHintComponent({
     Key? key,
     required this.text,
+    this.inlineWidgetBuilders = const [],
     this.hintText,
     this.hintStyleBuilder,
     this.textAlign,
@@ -705,6 +706,10 @@ class TextWithHintComponent extends StatefulWidget {
   }) : super(key: key);
 
   final AttributedText text;
+
+  /// {@macro text_component_inline_widget_builders}
+  final InlineWidgetBuilderChain inlineWidgetBuilders;
+
   final AttributedText? hintText;
   final AttributionStyleBuilder? hintStyleBuilder;
   final TextAlign? textAlign;
@@ -757,6 +762,7 @@ class _TextWithHintComponentState extends State<TextWithHintComponent>
         TextComponent(
           key: _childTextComponentKey,
           text: widget.text,
+          inlineWidgetBuilders: widget.inlineWidgetBuilders,
           textAlign: widget.textAlign,
           textDirection: widget.textDirection,
           textStyleBuilder: widget.textStyleBuilder,
@@ -805,10 +811,12 @@ class TextComponent extends StatefulWidget {
 
   final AttributionStyleBuilder textStyleBuilder;
 
+  /// {@template text_component_inline_widget_builders}
   /// A Chain of Responsibility that's used to build inline widgets.
   ///
   /// The first builder in the chain to return a non-null `Widget` will be
   /// used for a given inline placeholder.
+  /// {@endtemplate}
   final InlineWidgetBuilderChain inlineWidgetBuilders;
 
   final Map<String, dynamic> metadata;


Test patch:
diff --git a/super_editor/test/super_editor/components/hint_text_test.dart b/super_editor/test/super_editor/components/hint_text_test.dart
new file mode 100644
index 000000000..857829678
--- /dev/null
+++ b/super_editor/test/super_editor/components/hint_text_test.dart
@@ -0,0 +1,75 @@
+import 'package:flutter/material.dart' show Colors;
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter_test_runners/flutter_test_runners.dart';
+import 'package:super_editor/src/test/super_editor_test/supereditor_inspector.dart';
+import 'package:super_editor/super_editor.dart';
+
+import '../supereditor_test_tools.dart';
+
+void main() {
+  group("Super Editor > components > hint text >", () {
+    testWidgetsOnArbitraryDesktop("displays inline widgets", (tester) async {
+      await tester
+          .createDocument()
+          .withCustomContent(MutableDocument(
+            nodes: [
+              ParagraphNode(
+                  id: "1",
+                  text: AttributedText("Hello to ", null, {
+                    9: "fake_mention",
+                  }))
+            ],
+          ))
+          .withComponentBuilders([
+            const HintComponentBuilder("Hello", _hintStyler),
+            ...defaultComponentBuilders,
+          ])
+          .useStylesheet(defaultStylesheet.copyWith(
+            inlineWidgetBuilders: _inlineWidgetBuilders,
+          ))
+          .pump();
+
+      // Ensure that we really are using the hint text component.
+      expect(find.byType(TextWithHintComponent), findsOne);
+
+      final richText = SuperEditorInspector.findRichTextInParagraph("1");
+      expect(richText.children, isNotNull);
+
+      // Verify that we show the text in the node.
+      expect(richText.children!.first, isA<TextSpan>());
+      expect((richText.children!.first as TextSpan).text, "Hello to ");
+
+      // Verify that we built the inline widget for the place holder in the node.
+      expect(richText.children!.last, isA<WidgetSpan>());
+      expect((richText.children!.last as WidgetSpan).child, isA<_FakeInlineWidget>());
+    });
+  });
+}
+
+TextStyle _hintStyler(BuildContext _) => TextStyle();
+
+const _inlineWidgetBuilders = [
+  _buildFakeInlineWidget,
+];
+
+Widget? _buildFakeInlineWidget(BuildContext context, TextStyle style, Object placeholder) {
+  if (placeholder is! String || placeholder != "fake_mention") {
+    return null;
+  }
+
+  return const _FakeInlineWidget();
+}
+
+class _FakeInlineWidget extends StatelessWidget {
+  const _FakeInlineWidget();
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      width: 16,
+      height: 16,
+      color: Colors.red,
+    );
+  }
+}
diff --git a/super_editor/test/super_editor/supereditor_components_test.dart b/super_editor/test/super_editor/supereditor_components_test.dart
index 4643215e1..358a0b8b0 100644
--- a/super_editor/test/super_editor/supereditor_components_test.dart
+++ b/super_editor/test/super_editor/supereditor_components_test.dart
@@ -16,7 +16,7 @@ void main() {
       await tester //
           .createDocument()
           .withSingleEmptyParagraph()
-          .withAddedComponents([const HintTextComponentBuilder()])
+          .withAddedComponents([HintComponentBuilder("Hello", (_) => const TextStyle())])
           .autoFocus(false)
           .pump();
 
@@ -96,55 +96,6 @@ void main() {
   });
 }
 
-class HintTextComponentBuilder implements ComponentBuilder {
-  const HintTextComponentBuilder();
-
-  @override
-  SingleColumnLayoutComponentViewModel? createViewModel(Document document, DocumentNode node) {
-    // This component builder can work with the standard paragraph view model.
-    // We'll defer to the standard paragraph component builder to create it.
-    return null;
-  }
-
-  @override
-  Widget? createComponent(
-      SingleColumnDocumentComponentContext componentContext, SingleColumnLayoutComponentViewModel componentViewModel) {
-    if (componentViewModel is! ParagraphComponentViewModel) {
-      return null;
-    }
-
-    final textSelection = componentViewModel.selection;
-
-    return TextWithHintComponent(
-      key: componentContext.componentKey,
-      text: componentViewModel.text,
-      textStyleBuilder: defaultStyleBuilder,
-      metadata: componentViewModel.blockType != null
-          ? {
-              'blockType': componentViewModel.blockType,
-            }
-          : {},
-      // This is the text displayed as a hint.
-      hintText: AttributedText(
-        'this is hint text...',
-        AttributedSpans(
-          attributions: [
-            const SpanMarker(attribution: italicsAttribution, offset: 12, markerType: SpanMarkerType.start),
-            const SpanMarker(attribution: italicsAttribution, offset: 15, markerType: SpanMarkerType.end),
-          ],
-        ),
-      ),
-      // This is the function that selects styles for the hint text.
-      hintStyleBuilder: (Set<Attribution> attributions) => defaultStyleBuilder(attributions).copyWith(
-        color: const Color(0xFFDDDDDD),
-      ),
-      textSelection: textSelection,
-      selectionColor: componentViewModel.selectionColor,
-      underlines: componentViewModel.createUnderlines(),
-    );
-  }
-}
-
 /// Pump a SuperEditor containing an image which will render as an 100x100 box
 /// and content big enough to cause the document to be scrollable.
 Future<void> _pumpImageTestApp(
@@ -223,9 +174,4 @@ class _UnknownNode extends BlockNode {
   _UnknownNode copyAndReplaceMetadata(Map<String, dynamic> newMetadata) {
     return _UnknownNode(id: id);
   }
-
-  @override
-  _UnknownNode copy() {
-    return _UnknownNode(id: id);
-  }
 }