This is an automated email from the ASF dual-hosted git repository.

pabloem pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new 9ec68dd3df1 Multifile examples on frontend (#24859) (#24865)
9ec68dd3df1 is described below

commit 9ec68dd3df1fb8e602bafe055031b71a39ccc41d
Author: alexeyinkin <[email protected]>
AuthorDate: Wed Jan 11 23:17:48 2023 +0400

    Multifile examples on frontend (#24859) (#24865)
    
    * Multifile examples on frontend (#24859)
    
    * Make a wrapper local (#24859)
    
    * Revert sdk.g.dart deletion, minor fixes (#24859)
    
    * Fix after review (#24859)
---
 playground/frontend/lib/l10n/app_en.arb            |   2 +-
 .../example_list/example_item_actions.dart         |  50 +++---
 .../examples/components/multi_file_icon.dart}      |  30 ++--
 .../multifile_popover/multifile_popover.dart       |  68 -------
 .../multifile_popover_button.dart                  |  95 ----------
 .../widgets/editor_textarea_wrapper.dart           |  91 ++++------
 .../widgets/playground_page_body.dart              |   2 +-
 .../lib/playground_components.dart                 |   1 +
 .../lib/src/cache/example_cache.dart               |  23 +--
 .../lib/src/constants/sizes.dart                   |   1 +
 .../example_loaders/content_example_loader.dart    |   2 +-
 .../example_loaders/http_example_loader.dart       |   5 +-
 .../lib/src/controllers/playground_controller.dart |  50 +++---
 .../controllers/snippet_editing_controller.dart    | 199 ++++++++++-----------
 .../snippet_file_editing_controller.dart           | 151 ++++++++++++++++
 .../lib/src/models/example.dart                    |  11 +-
 .../content_example_loading_descriptor.dart        |  18 +-
 .../lib/src/models/example_view_options.dart       |   8 +-
 .../shared_file.dart => models/snippet_file.dart}  |  31 +++-
 .../lib/src/models/snippet_file.g.dart             |  20 +++
 .../repositories/code_client/grpc_code_client.dart |   7 +-
 .../example_client/grpc_example_client.dart        |  34 ++--
 .../lib/src/repositories/example_repository.dart   |   5 +-
 .../get_precompiled_object_code_response.dart      |   6 +-
 .../repositories/models/get_snippet_response.dart  |   4 +-
 .../src/repositories/models/run_code_request.dart  |   9 +-
 .../repositories/models/save_snippet_request.dart  |   4 +-
 ...ponse.dart => snippet_file_grpc_extension.dart} |  29 +--
 .../lib/src/widgets/complexity.dart                |   5 +-
 .../lib/src/widgets/run_or_cancel_button.dart      |   1 -
 .../lib/src/widgets/snippet_editor.dart            | 153 ++++++----------
 ...nippet_editor.dart => snippet_file_editor.dart} |  37 ++--
 .../lib/src/widgets/tab_header.dart                |   4 +-
 .../lib/src/widgets/tabbed_snippet_editor.dart     |  81 +++++++++
 .../widgets/{tab_header.dart => tabs/tab_bar.dart} |  27 ++-
 .../frontend/playground_components/pubspec.yaml    |   3 +-
 .../test/src/cache/example_cache_test.dart         |  41 +++--
 .../test/src/common/categories.dart                |  14 +-
 .../test/src/common/descriptors.dart               |  12 +-
 .../test/src/common/example_cache.mocks.dart       |   5 +-
 .../test/src/common/example_repository_mock.dart   |  17 +-
 .../src/common/example_repository_mock.mocks.dart  |  27 +--
 .../test/src/common/examples.dart                  |  42 +++--
 .../test/src/common/requests.dart                  |   5 +-
 .../src/controllers/example_loaders/common.dart    |   2 +-
 .../examples_loader_test.mocks.dart                |  77 +++-----
 .../example_loaders/http_example_loader_test.dart  |   2 +-
 .../http_example_loader_test.mocks.dart            |   5 +-
 .../controllers/playground_controller_test.dart    |  41 +++--
 .../playground_controller_test.mocks.dart          |   5 +-
 .../snippet_editing_controller_test.dart           |  93 +++++++---
 .../content_example_loading_descriptor_test.dart   |   9 +-
 .../src/repositories/code_repository_test.dart     |   3 +-
 .../src/repositories/example_repository_test.dart  |  25 ++-
 .../playground_components_dev/pubspec.yaml         |   2 +-
 playground/frontend/pubspec.lock                   |   9 +-
 playground/frontend/pubspec.yaml                   |   2 +-
 .../messages/models/set_content_message_test.dart  |  35 ++--
 58 files changed, 918 insertions(+), 822 deletions(-)

diff --git a/playground/frontend/lib/l10n/app_en.arb 
b/playground/frontend/lib/l10n/app_en.arb
index 538df437994..a49ba9ebc2c 100644
--- a/playground/frontend/lib/l10n/app_en.arb
+++ b/playground/frontend/lib/l10n/app_en.arb
@@ -195,7 +195,7 @@
   "@exampleDescription": {
     "description": "Description icon label"
   },
-  "exampleMultifile": "Multifile example info",
+  "exampleMultifile": "Multifile",
   "@exampleDescription": {
     "exampleMultifile": "Multifile icon label"
   },
diff --git 
a/playground/frontend/lib/modules/examples/components/example_list/example_item_actions.dart
 
b/playground/frontend/lib/modules/examples/components/example_list/example_item_actions.dart
index df0c8a83750..03f9d2732d4 100644
--- 
a/playground/frontend/lib/modules/examples/components/example_list/example_item_actions.dart
+++ 
b/playground/frontend/lib/modules/examples/components/example_list/example_item_actions.dart
@@ -25,7 +25,7 @@ import 'package:provider/provider.dart';
 import '../../../../src/assets/assets.gen.dart';
 import '../../models/popover_state.dart';
 import '../description_popover/description_popover_button.dart';
-import '../multifile_popover/multifile_popover_button.dart';
+import '../multi_file_icon.dart';
 
 class ExampleItemActions extends StatelessWidget {
   final ExampleBase example;
@@ -39,24 +39,15 @@ class ExampleItemActions extends StatelessWidget {
   Widget build(BuildContext context) {
     return Row(
       children: [
-        if (example.isMultiFile) multifilePopover,
-        if (example.usesEmulatedData) const _EmulatedDataIcon(),
+        if (example.isMultiFile) const _IconWrapper(MultiFileIcon()),
+        if (example.usesEmulatedData) const _IconWrapper(_EmulatedDataIcon()),
         if (example.complexity != null)
-          ComplexityWidget(complexity: example.complexity!),
+          _IconWrapper(ComplexityWidget(complexity: example.complexity!)),
         descriptionPopover,
       ],
     );
   }
 
-  Widget get multifilePopover => MultifilePopoverButton(
-        parentContext: parentContext,
-        example: example,
-        followerAnchor: Alignment.topLeft,
-        targetAnchor: Alignment.topRight,
-        onOpen: () => _setPopoverOpen(parentContext, true),
-        onClose: () => _setPopoverOpen(parentContext, false),
-      );
-
   Widget get descriptionPopover => DescriptionPopoverButton(
         parentContext: parentContext,
         example: example,
@@ -76,14 +67,31 @@ class _EmulatedDataIcon extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return Padding(
-      padding: const EdgeInsets.only(right: 8.0),
-      child: Tooltip(
-        message: 'intents.playground.usesEmulatedData'.tr(),
-        child: SvgPicture.asset(
-          Assets.streaming,
-          color: Theme.of(context).extension<BeamThemeExtension>()?.iconColor,
-        ),
+    return Tooltip(
+      message: 'intents.playground.usesEmulatedData'.tr(),
+      child: SvgPicture.asset(
+        Assets.streaming,
+        color: Theme.of(context).extension<BeamThemeExtension>()?.iconColor,
+      ),
+    );
+  }
+}
+
+/// A wrapper of a standard size for icons in the example list.
+class _IconWrapper extends StatelessWidget {
+  const _IconWrapper(this.child);
+
+  final Widget child;
+
+  static const double _iconSize = 30;
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: _iconSize,
+      width: _iconSize,
+      child: Center(
+        child: child,
       ),
     );
   }
diff --git 
a/playground/frontend/playground_components/lib/src/widgets/tab_header.dart 
b/playground/frontend/lib/modules/examples/components/multi_file_icon.dart
similarity index 66%
copy from 
playground/frontend/playground_components/lib/src/widgets/tab_header.dart
copy to playground/frontend/lib/modules/examples/components/multi_file_icon.dart
index 714f025814e..e24ffa5f39c 100644
--- a/playground/frontend/playground_components/lib/src/widgets/tab_header.dart
+++ b/playground/frontend/lib/modules/examples/components/multi_file_icon.dart
@@ -17,30 +17,22 @@
  */
 
 import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:flutter_svg/flutter_svg.dart';
 
-import '../constants/sizes.dart';
+import '../../../src/assets/assets.gen.dart';
 
-const kHeaderHeight = 50.0;
-
-class TabHeader extends StatelessWidget {
-  final TabController tabController;
-  final Widget tabsWidget;
-
-  const TabHeader({
-    super.key,
-    required this.tabController,
-    required this.tabsWidget,
-  });
+class MultiFileIcon extends StatelessWidget {
+  const MultiFileIcon();
 
   @override
   Widget build(BuildContext context) {
-    return SizedBox(
-      height: 50,
-      child: Padding(
-        padding: const EdgeInsets.symmetric(
-          horizontal: BeamSizes.size16,
-        ),
-        child: tabsWidget,
+    AppLocalizations appLocale = AppLocalizations.of(context)!;
+    return Semantics(
+      container: true,
+      child: Tooltip(
+        message: appLocale.exampleMultifile,
+        child: SvgPicture.asset(Assets.multifile),
       ),
     );
   }
diff --git 
a/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover.dart
 
b/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover.dart
deleted file mode 100644
index fb1c76fc0c7..00000000000
--- 
a/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover.dart
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:flutter/material.dart';
-import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:flutter_svg/flutter_svg.dart';
-import 'package:playground_components/playground_components.dart';
-import 'package:url_launcher/url_launcher.dart';
-
-import '../../../../constants/font_weight.dart';
-import '../../../../constants/sizes.dart';
-import '../../../../src/assets/assets.gen.dart';
-
-const kMultifileWidth = 300.0;
-
-class MultifilePopover extends StatelessWidget {
-  final ExampleBase example;
-
-  const MultifilePopover({Key? key, required this.example}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    AppLocalizations appLocale = AppLocalizations.of(context)!;
-    return SizedBox(
-      width: kMultifileWidth,
-      child: Card(
-        child: Padding(
-          padding: const EdgeInsets.all(kLgSpacing),
-          child: Wrap(
-            runSpacing: kMdSpacing,
-            children: [
-              Text(
-                appLocale.multifile,
-                style: const TextStyle(
-                  fontSize: kTitleFontSize,
-                  fontWeight: kBoldWeight,
-                ),
-              ),
-              Text(appLocale.multifileWarning),
-              TextButton.icon(
-                icon: SvgPicture.asset(Assets.github),
-                onPressed: () {
-                  launchUrl(Uri.parse(example.link ?? ''));
-                },
-                label: Text(appLocale.viewOnGithub),
-              ),
-            ],
-          ),
-        ),
-      ),
-    );
-  }
-}
diff --git 
a/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover_button.dart
 
b/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover_button.dart
deleted file mode 100644
index 78fbafdf51a..00000000000
--- 
a/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover_button.dart
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:aligned_dialog/aligned_dialog.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:flutter_svg/flutter_svg.dart';
-import 'package:playground_components/playground_components.dart';
-
-import '../../../../constants/sizes.dart';
-import '../../../../src/assets/assets.gen.dart';
-import 'multifile_popover.dart';
-
-class MultifilePopoverButton extends StatelessWidget {
-  final BuildContext? parentContext;
-  final ExampleBase example;
-  final Alignment followerAnchor;
-  final Alignment targetAnchor;
-  final void Function()? onOpen;
-  final void Function()? onClose;
-
-  const MultifilePopoverButton({
-    Key? key,
-    this.parentContext,
-    required this.example,
-    required this.followerAnchor,
-    required this.targetAnchor,
-    this.onOpen,
-    this.onClose,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    AppLocalizations appLocale = AppLocalizations.of(context)!;
-    return Semantics(
-      container: true,
-      child: IconButton(
-        iconSize: kIconSizeMd,
-        splashRadius: kIconButtonSplashRadius,
-        icon: SvgPicture.asset(Assets.multifile),
-        tooltip: appLocale.exampleMultifile,
-        onPressed: () {
-          _showMultifilePopover(
-            parentContext ?? context,
-            example,
-            followerAnchor,
-            targetAnchor,
-          );
-        },
-      ),
-    );
-  }
-
-  void _showMultifilePopover(
-    BuildContext context,
-    ExampleBase example,
-    Alignment followerAnchor,
-    Alignment targetAnchor,
-  ) async {
-    // close previous dialogs
-    Navigator.of(context, rootNavigator: true).popUntil((route) {
-      return route.isFirst;
-    });
-    if (onOpen != null) {
-      onOpen!();
-    }
-    await showAlignedDialog(
-      context: context,
-      builder: (dialogContext) => MultifilePopover(
-        example: example,
-      ),
-      followerAnchor: followerAnchor,
-      targetAnchor: targetAnchor,
-      barrierColor: Colors.transparent,
-    );
-    if (onClose != null) {
-      onClose!();
-    }
-  }
-}
diff --git 
a/playground/frontend/lib/pages/standalone_playground/widgets/editor_textarea_wrapper.dart
 
b/playground/frontend/lib/pages/standalone_playground/widgets/editor_textarea_wrapper.dart
index 5ee81cb05cf..86b6a5045d6 100644
--- 
a/playground/frontend/lib/pages/standalone_playground/widgets/editor_textarea_wrapper.dart
+++ 
b/playground/frontend/lib/pages/standalone_playground/widgets/editor_textarea_wrapper.dart
@@ -24,84 +24,59 @@ import 
'../../../components/playground_run_or_cancel_button.dart';
 import '../../../constants/sizes.dart';
 import '../../../modules/editor/components/share_dropdown/share_button.dart';
 import 
'../../../modules/examples/components/description_popover/description_popover_button.dart';
-import 
'../../../modules/examples/components/multifile_popover/multifile_popover_button.dart';
 
 /// A code editor with controls stacked above it.
 class CodeTextAreaWrapper extends StatelessWidget {
-  final PlaygroundController controller;
+  final PlaygroundController playgroundController;
 
   const CodeTextAreaWrapper({
-    required this.controller,
+    required this.playgroundController,
   });
 
   @override
   Widget build(BuildContext context) {
-    if (controller.result?.errorMessage?.isNotEmpty ?? false) {
+    if (playgroundController.result?.errorMessage?.isNotEmpty ?? false) {
       WidgetsBinding.instance.addPostFrameCallback((_) {
-        _handleError(context, controller);
+        _handleError(context, playgroundController);
       });
     }
 
-    final snippetController = controller.snippetEditingController;
+    final snippetController = playgroundController.snippetEditingController;
 
     if (snippetController == null) {
       return const LoadingIndicator();
     }
 
-    return Column(
-      children: [
-        Expanded(
-          child: Stack(
-            children: [
-              Positioned.fill(
-                child: SnippetEditor(
-                  controller: snippetController,
-                  isEditable: true,
-                ),
-              ),
-              Positioned(
-                right: kXlSpacing,
-                top: kXlSpacing,
-                height: kButtonHeight,
-                child: Row(
-                  children: [
-                    if (controller.selectedExample != null) ...[
-                      if (controller.selectedExample?.isMultiFile ?? false)
-                        Semantics(
-                          container: true,
-                          child: MultifilePopoverButton(
-                            example: controller.selectedExample!,
-                            followerAnchor: Alignment.topRight,
-                            targetAnchor: Alignment.bottomRight,
-                          ),
-                        ),
-                      Semantics(
-                        container: true,
-                        child: DescriptionPopoverButton(
-                          example: controller.selectedExample!,
-                          followerAnchor: Alignment.topRight,
-                          targetAnchor: Alignment.bottomRight,
-                        ),
-                      ),
-                    ],
-                    Semantics(
-                      container: true,
-                      child: ShareButton(
-                        playgroundController: controller,
-                      ),
-                    ),
-                    const SizedBox(width: kLgSpacing),
-                    Semantics(
-                      container: true,
-                      child: const PlaygroundRunOrCancelButton(),
-                    ),
-                  ],
-                ),
+    final example = snippetController.example;
+
+    return SnippetEditor(
+      controller: snippetController,
+      isEditable: true,
+      actionsWidget: Row(
+        children: [
+          if (example != null)
+            Semantics(
+              container: true,
+              child: DescriptionPopoverButton(
+                example: example,
+                followerAnchor: Alignment.topRight,
+                targetAnchor: Alignment.bottomRight,
               ),
-            ],
+            ),
+          Semantics(
+            container: true,
+            child: ShareButton(
+              playgroundController: playgroundController,
+            ),
           ),
-        ),
-      ],
+          const SizedBox(width: kLgSpacing),
+          Semantics(
+            container: true,
+            child: const PlaygroundRunOrCancelButton(),
+          ),
+          const SizedBox(width: kLgSpacing),
+        ],
+      ),
     );
   }
 
diff --git 
a/playground/frontend/lib/pages/standalone_playground/widgets/playground_page_body.dart
 
b/playground/frontend/lib/pages/standalone_playground/widgets/playground_page_body.dart
index 2ce17d79c35..a027f72e52e 100644
--- 
a/playground/frontend/lib/pages/standalone_playground/widgets/playground_page_body.dart
+++ 
b/playground/frontend/lib/pages/standalone_playground/widgets/playground_page_body.dart
@@ -45,7 +45,7 @@ class PlaygroundPageBody extends StatelessWidget {
       );
 
       final codeTextArea = CodeTextAreaWrapper(
-        controller: controller,
+        playgroundController: controller,
       );
 
       switch (outputState.placement) {
diff --git 
a/playground/frontend/playground_components/lib/playground_components.dart 
b/playground/frontend/playground_components/lib/playground_components.dart
index 007a3ec29b6..8d37335e9cc 100644
--- a/playground/frontend/playground_components/lib/playground_components.dart
+++ b/playground/frontend/playground_components/lib/playground_components.dart
@@ -44,6 +44,7 @@ export 'src/models/loading_status.dart';
 export 'src/models/outputs.dart';
 export 'src/models/sdk.dart';
 export 'src/models/shortcut.dart';
+export 'src/models/snippet_file.dart';
 export 'src/models/toast.dart';
 export 'src/models/toast_type.dart';
 
diff --git 
a/playground/frontend/playground_components/lib/src/cache/example_cache.dart 
b/playground/frontend/playground_components/lib/src/cache/example_cache.dart
index d42753b4c27..ee59253a269 100644
--- a/playground/frontend/playground_components/lib/src/cache/example_cache.dart
+++ b/playground/frontend/playground_components/lib/src/cache/example_cache.dart
@@ -29,13 +29,13 @@ import '../models/example.dart';
 import '../models/example_base.dart';
 import '../models/loading_status.dart';
 import '../models/sdk.dart';
+import '../models/snippet_file.dart';
 import '../repositories/example_repository.dart';
 import '../repositories/models/get_default_precompiled_object_request.dart';
 import '../repositories/models/get_precompiled_object_request.dart';
 import '../repositories/models/get_precompiled_objects_request.dart';
 import '../repositories/models/get_snippet_request.dart';
 import '../repositories/models/save_snippet_request.dart';
-import '../repositories/models/shared_file.dart';
 
 /// A runtime cache for examples fetched from a repository.
 class ExampleCache extends ChangeNotifier {
@@ -100,7 +100,7 @@ class ExampleCache extends ChangeNotifier {
     );
   }
 
-  Future<String> _getPrecompiledObjectCode(String path, Sdk sdk) {
+  Future<List<SnippetFile>> _getPrecompiledObjectCode(String path, Sdk sdk) {
     return _exampleRepository.getPrecompiledObjectCode(
       GetPrecompiledObjectRequest(path: path, sdk: sdk),
     );
@@ -125,17 +125,17 @@ class ExampleCache extends ChangeNotifier {
 
     return Example(
       complexity: result.complexity,
+      files: result.files,
       name: result.files.first.name,
       path: id,
       sdk: result.sdk,
-      source: result.files.first.code,
       pipelineOptions: result.pipelineOptions,
       type: ExampleType.example,
     );
   }
 
   Future<String> saveSnippet({
-    required List<SharedFile> files,
+    required List<SnippetFile> files,
     required Sdk sdk,
     required String pipelineOptions,
   }) async {
@@ -170,12 +170,13 @@ class ExampleCache extends ChangeNotifier {
 
       return Example.fromBase(
         example,
-        source: exampleData[0],
-        outputs: exampleData[1],
-        logs: exampleData[2],
+        files: exampleData[0] as List<SnippetFile>,
+        outputs: exampleData[1] as String,
+        logs: exampleData[2] as String,
       );
     }
 
+    // TODO(alexeyinkin): Load in a single request, 
https://github.com/apache/beam/issues/24305
     final exampleData = await Future.wait([
       _getPrecompiledObjectCode(example.path, example.sdk),
       _getPrecompiledObjectOutput(example.path, example.sdk),
@@ -185,10 +186,10 @@ class ExampleCache extends ChangeNotifier {
 
     return Example.fromBase(
       example,
-      source: exampleData[0],
-      outputs: exampleData[1],
-      logs: exampleData[2],
-      graph: exampleData[3],
+      files: exampleData[0] as List<SnippetFile>,
+      outputs: exampleData[1] as String,
+      logs: exampleData[2] as String,
+      graph: exampleData[3] as String,
     );
   }
 
diff --git 
a/playground/frontend/playground_components/lib/src/constants/sizes.dart 
b/playground/frontend/playground_components/lib/src/constants/sizes.dart
index b7ebd6eb38a..88e69669567 100644
--- a/playground/frontend/playground_components/lib/src/constants/sizes.dart
+++ b/playground/frontend/playground_components/lib/src/constants/sizes.dart
@@ -41,6 +41,7 @@ class BeamSizes {
   static const double headerButtonHeight = 46;
   static const double loadingIndicator = 40;
   static const double splitViewSeparator = BeamSizes.size8;
+  static const double tabBarHeight = 50;
 }
 
 class BeamBorderRadius {
diff --git 
a/playground/frontend/playground_components/lib/src/controllers/example_loaders/content_example_loader.dart
 
b/playground/frontend/playground_components/lib/src/controllers/example_loaders/content_example_loader.dart
index 53f0288d013..bc629e07001 100644
--- 
a/playground/frontend/playground_components/lib/src/controllers/example_loaders/content_example_loader.dart
+++ 
b/playground/frontend/playground_components/lib/src/controllers/example_loaders/content_example_loader.dart
@@ -42,10 +42,10 @@ class ContentExampleLoader extends ExampleLoader {
   @override
   Future<Example> get future async => Example(
         complexity: descriptor.complexity,
+        files: descriptor.files,
         name: descriptor.name ?? 'Untitled Example',
         path: '',
         sdk: descriptor.sdk,
-        source: descriptor.content,
         type: ExampleType.example,
       );
 }
diff --git 
a/playground/frontend/playground_components/lib/src/controllers/example_loaders/http_example_loader.dart
 
b/playground/frontend/playground_components/lib/src/controllers/example_loaders/http_example_loader.dart
index 6f95c2e9b77..a3a1887f459 100644
--- 
a/playground/frontend/playground_components/lib/src/controllers/example_loaders/http_example_loader.dart
+++ 
b/playground/frontend/playground_components/lib/src/controllers/example_loaders/http_example_loader.dart
@@ -24,6 +24,7 @@ import '../../models/example.dart';
 import '../../models/example_base.dart';
 import 
'../../models/example_loading_descriptors/http_example_loading_descriptor.dart';
 import '../../models/sdk.dart';
+import '../../models/snippet_file.dart';
 import 'example_loader.dart';
 
 /// The [ExampleLoader] for [HttpExampleLoadingDescriptor].
@@ -48,9 +49,11 @@ class HttpExampleLoader extends ExampleLoader {
 
     return Example(
       name: descriptor.uri.path.split('/').lastOrNull ?? 'HTTP Example',
+      files: [
+        SnippetFile(content: response.body, isMain: true),
+      ],
       path: descriptor.uri.toString(),
       sdk: descriptor.sdk,
-      source: response.body,
       type: ExampleType.example,
       viewOptions: descriptor.viewOptions,
     );
diff --git 
a/playground/frontend/playground_components/lib/src/controllers/playground_controller.dart
 
b/playground/frontend/playground_components/lib/src/controllers/playground_controller.dart
index 463c7607dda..86aa44f86b9 100644
--- 
a/playground/frontend/playground_components/lib/src/controllers/playground_controller.dart
+++ 
b/playground/frontend/playground_components/lib/src/controllers/playground_controller.dart
@@ -19,7 +19,6 @@
 import 'dart:async';
 import 'dart:math';
 
-import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:get_it/get_it.dart';
@@ -39,7 +38,6 @@ import '../models/shortcut.dart';
 import '../repositories/code_repository.dart';
 import '../repositories/models/run_code_request.dart';
 import '../repositories/models/run_code_result.dart';
-import '../repositories/models/shared_file.dart';
 import '../services/symbols/loaders/map.dart';
 import '../services/symbols/symbols_notifier.dart';
 import '../util/pipeline_options.dart';
@@ -105,11 +103,11 @@ class PlaygroundController with ChangeNotifier {
 
   // TODO(alexeyinkin): Return full, then shorten, 
https://github.com/apache/beam/issues/23250
   String get examplesTitle {
-    final name = snippetEditingController?.selectedExample?.name ?? kTitle;
+    final name = snippetEditingController?.example?.name ?? kTitle;
     return name.substring(0, min(kTitleLength, name.length));
   }
 
-  Example? get selectedExample => snippetEditingController?.selectedExample;
+  Example? get selectedExample => snippetEditingController?.example;
 
   Sdk? get sdk => _sdk;
 
@@ -126,7 +124,8 @@ class PlaygroundController with ChangeNotifier {
     return controller;
   }
 
-  String? get source => snippetEditingController?.codeController.fullText;
+  String? get source =>
+      snippetEditingController?.activeFileController?.codeController.fullText;
 
   bool get isCodeRunning => !(result?.isFinished ?? true);
 
@@ -266,12 +265,6 @@ class PlaygroundController with ChangeNotifier {
     GetIt.instance.get<SymbolsNotifier>().addLoaderIfNot(mode, loader);
   }
 
-  // TODO(alexeyinkin): Remove, used only in tests, refactor them.
-  void setSource(String source) {
-    final controller = requireSnippetEditingController();
-    controller.setSource(source);
-  }
-
   void setSelectedOutputFilterType(OutputType type) {
     selectedOutputFilterType = type;
     notifyListeners();
@@ -322,14 +315,14 @@ class PlaygroundController with ChangeNotifier {
     }
     _executionTime?.close();
     _executionTime = _createExecutionTimeStream();
-    if (!isExampleChanged && controller.selectedExample?.outputs != null) {
+    if (!isExampleChanged && controller.example?.outputs != null) {
       _showPrecompiledResult(controller);
     } else {
       final request = RunCodeRequest(
-        code: controller.codeController.fullText,
-        sdk: controller.sdk,
-        pipelineOptions: parsedPipelineOptions,
         datasets: selectedExample?.datasets ?? [],
+        files: controller.getFiles(),
+        pipelineOptions: parsedPipelineOptions,
+        sdk: controller.sdk,
       );
       _runSubscription = _codeRepository?.runCode(request).listen((event) {
         _result = event;
@@ -373,7 +366,7 @@ class PlaygroundController with ChangeNotifier {
     _result = const RunCodeResult(
       status: RunCodeStatus.preparation,
     );
-    final selectedExample = snippetEditingController.selectedExample!;
+    final selectedExample = snippetEditingController.example!;
 
     notifyListeners();
     // add a little delay to improve user experience
@@ -439,25 +432,22 @@ class PlaygroundController with ChangeNotifier {
   }
 
   Future<UserSharedExampleLoadingDescriptor> saveSnippet() async {
-    final controller = requireSnippetEditingController();
-    final code = controller.codeController.fullText;
-    final name = 'examples.userSharedName'.tr();
+    final snippetController = requireSnippetEditingController();
+    final files = snippetController.getFiles();
 
     final snippetId = await exampleCache.saveSnippet(
-      files: [
-        SharedFile(code: code, isMain: true, name: name),
-      ],
-      sdk: controller.sdk,
-      pipelineOptions: controller.pipelineOptions,
+      files: files,
+      pipelineOptions: snippetController.pipelineOptions,
+      sdk: snippetController.sdk,
     );
 
     final sharedExample = Example(
-      datasets: controller.selectedExample?.datasets ?? [],
-      source: code,
-      name: name,
-      sdk: controller.sdk,
-      type: ExampleType.example,
+      datasets: snippetController.example?.datasets ?? [],
+      files: files,
+      name: files.first.name,
       path: snippetId,
+      sdk: snippetController.sdk,
+      type: ExampleType.example,
     );
 
     final descriptor = UserSharedExampleLoadingDescriptor(
@@ -465,7 +455,7 @@ class PlaygroundController with ChangeNotifier {
       snippetId: snippetId,
     );
 
-    controller.setExample(sharedExample, descriptor: descriptor);
+    snippetController.setExample(sharedExample, descriptor: descriptor);
 
     return descriptor;
   }
diff --git 
a/playground/frontend/playground_components/lib/src/controllers/snippet_editing_controller.dart
 
b/playground/frontend/playground_components/lib/src/controllers/snippet_editing_controller.dart
index 052ee41aea1..c04d10cfb41 100644
--- 
a/playground/frontend/playground_components/lib/src/controllers/snippet_editing_controller.dart
+++ 
b/playground/frontend/playground_components/lib/src/controllers/snippet_editing_controller.dart
@@ -16,11 +16,8 @@
  * limitations under the License.
  */
 
-import 'dart:math';
-
+import 'package:collection/collection.dart';
 import 'package:flutter/widgets.dart';
-import 'package:flutter_code_editor/flutter_code_editor.dart';
-import 'package:get_it/get_it.dart';
 
 import '../models/example.dart';
 import 
'../models/example_loading_descriptors/content_example_loading_descriptor.dart';
@@ -29,43 +26,27 @@ import 
'../models/example_loading_descriptors/example_loading_descriptor.dart';
 import '../models/example_view_options.dart';
 import '../models/loading_status.dart';
 import '../models/sdk.dart';
-import '../services/symbols/symbols_notifier.dart';
+import '../models/snippet_file.dart';
+import 'snippet_file_editing_controller.dart';
 
 /// The main state object for a single [sdk].
 class SnippetEditingController extends ChangeNotifier {
   final Sdk sdk;
-  final CodeController codeController;
-  final _symbolsNotifier = GetIt.instance.get<SymbolsNotifier>();
-  Example? _selectedExample;
+
   ExampleLoadingDescriptor? _descriptor;
+  Example? _example;
   String _pipelineOptions = '';
+
   bool _isChanged = false;
   LoadingStatus _exampleLoadingStatus = LoadingStatus.done;
 
+  SnippetFileEditingController? _activeFileController;
+  final _fileControllers = <SnippetFileEditingController>[];
+  final _fileControllersByName = <String, SnippetFileEditingController>{};
+
   SnippetEditingController({
     required this.sdk,
-  }) : codeController = CodeController(
-          language: sdk.highlightMode,
-          namedSectionParser: const BracketsStartEndNamedSectionParser(),
-        ) {
-    codeController.addListener(_onCodeControllerChanged);
-    _symbolsNotifier.addListener(_onSymbolsNotifierChanged);
-    _onSymbolsNotifierChanged();
-  }
-
-  void _onCodeControllerChanged() {
-    if (!_isChanged) {
-      if (_isCodeChanged()) {
-        _isChanged = true;
-        notifyListeners();
-      }
-    } else {
-      _updateIsChanged();
-      if (!_isChanged) {
-        notifyListeners();
-      }
-    }
-  }
+  });
 
   /// Attempts to acquire a lock for asynchronous example loading.
   ///
@@ -95,66 +76,18 @@ class SnippetEditingController extends ChangeNotifier {
     ExampleLoadingDescriptor? descriptor,
   }) {
     _descriptor = descriptor;
-    _selectedExample = example;
+    _example = example;
     _pipelineOptions = example.pipelineOptions;
     _isChanged = false;
     releaseExampleLoading();
 
-    final viewOptions = example.viewOptions;
-
-    codeController.removeListener(_onCodeControllerChanged);
-    setSource(example.source);
-    _applyViewOptions(viewOptions);
-    _toStartOfContextLineIfAny();
-    codeController.addListener(_onCodeControllerChanged);
+    _deleteFileControllers();
+    _createFileControllers(example.files, example.viewOptions);
 
     notifyListeners();
   }
 
-  void _applyViewOptions(ExampleViewOptions options) {
-    codeController.readOnlySectionNames = options.readOnlySectionNames.toSet();
-    codeController.visibleSectionNames = options.showSectionNames.toSet();
-
-    if (options.foldCommentAtLineZero) {
-      codeController.foldCommentAtLineZero();
-    }
-
-    if (options.foldImports) {
-      codeController.foldImports();
-    }
-
-    final unfolded = options.unfoldSectionNames;
-    if (unfolded.isNotEmpty) {
-      codeController.foldOutsideSections(unfolded);
-    }
-  }
-
-  void _toStartOfContextLineIfAny() {
-    final contextLine1Based = selectedExample?.contextLine;
-
-    if (contextLine1Based == null) {
-      return;
-    }
-
-    _toStartOfFullLine(max(contextLine1Based - 1, 0));
-  }
-
-  void _toStartOfFullLine(int line) {
-    if (line >= codeController.code.lines.length) {
-      return;
-    }
-
-    final fullPosition = codeController.code.lines.lines[line].textRange.start;
-    final visiblePosition = codeController.code.hiddenRanges.cutPosition(
-      fullPosition,
-    );
-
-    codeController.selection = TextSelection.collapsed(
-      offset: visiblePosition,
-    );
-  }
-
-  Example? get selectedExample => _selectedExample;
+  Example? get example => _example;
 
   ExampleLoadingDescriptor? get descriptor => _descriptor;
 
@@ -182,26 +115,33 @@ class SnippetEditingController extends ChangeNotifier {
   bool get isChanged => _isChanged;
 
   void _updateIsChanged() {
-    _isChanged = _isCodeChanged() || _arePipelineOptionsChanged();
+    _isChanged = _calculateIsChanged();
   }
 
-  bool _isCodeChanged() {
-    return _selectedExample?.source != codeController.fullText;
+  bool _calculateIsChanged() {
+    return _isAnyFileControllerChanged() || _arePipelineOptionsChanged();
+  }
+
+  bool _isAnyFileControllerChanged() {
+    return _fileControllers.any((c) => c.isChanged);
   }
 
   bool _arePipelineOptionsChanged() {
-    return _pipelineOptions != (_selectedExample?.pipelineOptions ?? '');
+    return _pipelineOptions != (_example?.pipelineOptions ?? '');
   }
 
   void reset() {
-    codeController.text = _selectedExample?.source ?? '';
-    _pipelineOptions = _selectedExample?.pipelineOptions ?? '';
+    for (final controller in _fileControllers) {
+      controller.reset();
+    }
+
+    _pipelineOptions = _example?.pipelineOptions ?? '';
   }
 
   /// Creates an [ExampleLoadingDescriptor] that can recover the
   /// current content.
   ExampleLoadingDescriptor getLoadingDescriptor() {
-    final example = selectedExample;
+    final example = this.example;
     if (example == null) {
       return EmptyExampleLoadingDescriptor(sdk: sdk);
     }
@@ -212,39 +152,80 @@ class SnippetEditingController extends ChangeNotifier {
 
     return ContentExampleLoadingDescriptor(
       complexity: example.complexity,
-      content: codeController.fullText,
+      files: getFiles(),
       name: example.name,
       sdk: sdk,
     );
   }
 
-  void setSource(String source) {
-    codeController.readOnlySectionNames = const {};
-    codeController.visibleSectionNames = const {};
+  void _deleteFileControllers() {
+    for (final controller in _fileControllers) {
+      controller.removeListener(_onFileControllerChanged);
+      controller.dispose();
+    }
 
-    codeController.fullText = source;
-    codeController.historyController.deleteHistory();
+    _fileControllers.clear();
+    _fileControllersByName.clear();
   }
 
-  void _onSymbolsNotifierChanged() {
-    final mode = sdk.highlightMode;
-    if (mode == null) {
-      return;
+  void _createFileControllers(
+    Iterable<SnippetFile> files,
+    ExampleViewOptions viewOptions,
+  ) {
+    for (final file in files) {
+      final controller = SnippetFileEditingController(
+        contextLine1Based: file.isMain ? _example?.contextLine : null,
+        savedFile: file,
+        sdk: sdk,
+        viewOptions: viewOptions,
+      );
+
+      _fileControllers.add(controller);
+      controller.addListener(_onFileControllerChanged);
     }
 
-    final dictionary = _symbolsNotifier.getDictionary(mode);
-    if (dictionary == null) {
-      return;
+    for (final controller in _fileControllers) {
+      _fileControllersByName[controller.savedFile.name] = controller;
     }
 
-    codeController.autocompleter.setCustomWords(dictionary.symbols);
+    _activeFileController =
+        _fileControllers.firstWhereOrNull((c) => c.savedFile.isMain);
   }
 
-  @override
-  void dispose() {
-    _symbolsNotifier.removeListener(
-      _onSymbolsNotifierChanged,
-    );
-    super.dispose();
+  void _onFileControllerChanged() {
+    if (!_isChanged) {
+      if (_isAnyFileControllerChanged()) {
+        _isChanged = true;
+        notifyListeners();
+      }
+    } else {
+      _updateIsChanged();
+      if (!_isChanged) {
+        notifyListeners();
+      }
+    }
+  }
+
+  List<SnippetFileEditingController> get fileControllers =>
+      UnmodifiableListView(_fileControllers);
+
+  SnippetFileEditingController? get activeFileController =>
+      _activeFileController;
+
+  SnippetFileEditingController? getFileControllerByName(String name) {
+    return _fileControllersByName[name];
+  }
+
+  void activateFileControllerByName(String name) {
+    final newController = getFileControllerByName(name);
+
+    if (newController != _activeFileController) {
+      _activeFileController = newController;
+      notifyListeners();
+    }
+  }
+
+  List<SnippetFile> getFiles() {
+    return _fileControllers.map((c) => c.getFile()).toList(growable: false);
   }
 }
diff --git 
a/playground/frontend/playground_components/lib/src/controllers/snippet_file_editing_controller.dart
 
b/playground/frontend/playground_components/lib/src/controllers/snippet_file_editing_controller.dart
new file mode 100644
index 00000000000..d6802b9655c
--- /dev/null
+++ 
b/playground/frontend/playground_components/lib/src/controllers/snippet_file_editing_controller.dart
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'dart:math';
+
+import 'package:flutter/widgets.dart';
+import 'package:flutter_code_editor/flutter_code_editor.dart';
+import 'package:get_it/get_it.dart';
+
+import '../models/example_view_options.dart';
+import '../models/sdk.dart';
+import '../models/snippet_file.dart';
+import '../services/symbols/symbols_notifier.dart';
+
+/// The main state object for a file in a snippet.
+class SnippetFileEditingController extends ChangeNotifier {
+  final CodeController codeController;
+  final SnippetFile savedFile;
+  final Sdk sdk;
+
+  bool _isChanged = false;
+
+  final _symbolsNotifier = GetIt.instance.get<SymbolsNotifier>();
+
+  SnippetFileEditingController({
+    required this.savedFile,
+    required this.sdk,
+    required ExampleViewOptions viewOptions,
+    int? contextLine1Based,
+  }) : codeController = CodeController(
+          language: sdk.highlightMode,
+          namedSectionParser: const BracketsStartEndNamedSectionParser(),
+          text: savedFile.content,
+        ) {
+    _applyViewOptions(viewOptions);
+
+    // TODO(alexeyinkin): Scroll to a comment instead of index,
+    //  then remove the parameter, https://github.com/apache/beam/issues/23774
+    if (contextLine1Based != null) {
+      _toStartOfFullLine(max(contextLine1Based - 1, 0));
+    }
+
+    codeController.addListener(_onCodeControllerChanged);
+    _symbolsNotifier.addListener(_onSymbolsNotifierChanged);
+    _onSymbolsNotifierChanged();
+  }
+
+  void _applyViewOptions(ExampleViewOptions options) {
+    codeController.readOnlySectionNames = options.readOnlySectionNames.toSet();
+    codeController.visibleSectionNames = options.showSectionNames.toSet();
+
+    if (options.foldCommentAtLineZero) {
+      codeController.foldCommentAtLineZero();
+    }
+
+    if (options.foldImports) {
+      codeController.foldImports();
+    }
+
+    final unfolded = options.unfoldSectionNames;
+    if (unfolded.isNotEmpty) {
+      codeController.foldOutsideSections(unfolded);
+    }
+  }
+
+  void _toStartOfFullLine(int line) {
+    if (line >= codeController.code.lines.length) {
+      return;
+    }
+
+    final fullPosition = codeController.code.lines.lines[line].textRange.start;
+    final visiblePosition = codeController.code.hiddenRanges.cutPosition(
+      fullPosition,
+    );
+
+    codeController.selection = TextSelection.collapsed(
+      offset: visiblePosition,
+    );
+  }
+
+  void _onCodeControllerChanged() {
+    if (!_isChanged) {
+      if (_isCodeChanged()) {
+        _isChanged = true;
+        notifyListeners();
+      }
+    } else {
+      _updateIsChanged();
+      if (!_isChanged) {
+        notifyListeners();
+      }
+    }
+  }
+
+  bool get isChanged => _isChanged;
+
+  bool _isCodeChanged() {
+    return savedFile.content != codeController.fullText;
+  }
+
+  void _updateIsChanged() {
+    _isChanged = _isCodeChanged();
+  }
+
+  void reset() {
+    codeController.text = savedFile.content;
+  }
+
+  void _onSymbolsNotifierChanged() {
+    final mode = sdk.highlightMode;
+    if (mode == null) {
+      return;
+    }
+
+    final dictionary = _symbolsNotifier.getDictionary(mode);
+    if (dictionary == null) {
+      return;
+    }
+
+    codeController.autocompleter.setCustomWords(dictionary.symbols);
+  }
+
+  SnippetFile getFile() => SnippetFile(
+        content: codeController.fullText,
+        isMain: savedFile.isMain,
+        name: savedFile.name,
+      );
+
+  @override
+  void dispose() {
+    _symbolsNotifier.removeListener(
+      _onSymbolsNotifierChanged,
+    );
+    super.dispose();
+  }
+}
diff --git 
a/playground/frontend/playground_components/lib/src/models/example.dart 
b/playground/frontend/playground_components/lib/src/models/example.dart
index 3f3d89c313b..aa24e82c4c4 100644
--- a/playground/frontend/playground_components/lib/src/models/example.dart
+++ b/playground/frontend/playground_components/lib/src/models/example.dart
@@ -18,16 +18,17 @@
 
 import 'example_base.dart';
 import 'sdk.dart';
+import 'snippet_file.dart';
 
 /// A [ExampleBase] that also has all large fields fetched.
 class Example extends ExampleBase {
+  final List<SnippetFile> files;
   final String? graph;
   final String? logs;
   final String? outputs;
-  final String source;
 
   const Example({
-    required this.source,
+    required this.files,
     required super.name,
     required super.sdk,
     required super.type,
@@ -48,9 +49,9 @@ class Example extends ExampleBase {
 
   Example.fromBase(
     ExampleBase example, {
+    required this.files,
     required this.logs,
     required this.outputs,
-    required this.source,
     this.graph,
   }) : super(
           complexity: example.complexity,
@@ -68,12 +69,12 @@ class Example extends ExampleBase {
           viewOptions: example.viewOptions,
         );
 
-  const Example.empty(Sdk sdk)
+  Example.empty(Sdk sdk)
       : this(
           name: 'Untitled Example',
+          files: [SnippetFile.empty],
           path: '',
           sdk: sdk,
-          source: '',
           type: ExampleType.example,
         );
 }
diff --git 
a/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/content_example_loading_descriptor.dart
 
b/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/content_example_loading_descriptor.dart
index 531c1bf5a41..b86336023ae 100644
--- 
a/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/content_example_loading_descriptor.dart
+++ 
b/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/content_example_loading_descriptor.dart
@@ -19,12 +19,12 @@
 import '../../enums/complexity.dart';
 import '../example_view_options.dart';
 import '../sdk.dart';
+import '../snippet_file.dart';
 import 'example_loading_descriptor.dart';
 
 /// Fully contains an example data to be loaded.
 class ContentExampleLoadingDescriptor extends ExampleLoadingDescriptor {
-  /// The source code.
-  final String content;
+  final List<SnippetFile> files;
 
   /// The name of the example, if any, to show in the dropdown.
   final String? name;
@@ -34,7 +34,7 @@ class ContentExampleLoadingDescriptor extends 
ExampleLoadingDescriptor {
   final Sdk sdk;
 
   const ContentExampleLoadingDescriptor({
-    required this.content,
+    required this.files,
     required this.sdk,
     this.complexity,
     this.name,
@@ -42,8 +42,8 @@ class ContentExampleLoadingDescriptor extends 
ExampleLoadingDescriptor {
   });
 
   static ContentExampleLoadingDescriptor? tryParse(Map<String, dynamic> map) {
-    final content = map['content']?.toString();
-    if (content == null) {
+    final files = map['files'];
+    if (files is! List) {
       return null;
     }
 
@@ -53,7 +53,9 @@ class ContentExampleLoadingDescriptor extends 
ExampleLoadingDescriptor {
     }
 
     return ContentExampleLoadingDescriptor(
-      content: content,
+      files: (map['files'] as List<dynamic>)
+          .map((file) => SnippetFile.fromJson(file as Map<String, dynamic>))
+          .toList(growable: false),
       name: map['name']?.toString(),
       sdk: sdk,
       complexity: Complexity.fromString(map['complexity']),
@@ -64,7 +66,7 @@ class ContentExampleLoadingDescriptor extends 
ExampleLoadingDescriptor {
   @override
   List<Object?> get props => [
         complexity,
-        content,
+        files,
         name,
         sdk.id,
       ];
@@ -72,7 +74,7 @@ class ContentExampleLoadingDescriptor extends 
ExampleLoadingDescriptor {
   @override
   Map<String, dynamic> toJson() => {
         'complexity': complexity?.name,
-        'content': content,
+        'files': files.map((e) => e.toJson()).toList(growable: false),
         'name': name,
         'sdk': sdk.id,
       };
diff --git 
a/playground/frontend/playground_components/lib/src/models/example_view_options.dart
 
b/playground/frontend/playground_components/lib/src/models/example_view_options.dart
index bc12a66db0a..b3d02a3ae86 100644
--- 
a/playground/frontend/playground_components/lib/src/models/example_view_options.dart
+++ 
b/playground/frontend/playground_components/lib/src/models/example_view_options.dart
@@ -28,17 +28,15 @@ class ExampleViewOptions with EquatableMixin {
   final List<String> unfoldSectionNames;
 
   const ExampleViewOptions({
-    required this.foldCommentAtLineZero,
-    required this.foldImports,
     required this.readOnlySectionNames,
     required this.showSectionNames,
     required this.unfoldSectionNames,
+    this.foldCommentAtLineZero = true,
+    this.foldImports = true,
   });
 
   factory ExampleViewOptions.fromShortMap(Map<String, dynamic> map) {
     return ExampleViewOptions(
-      foldCommentAtLineZero: true,
-      foldImports: true,
       readOnlySectionNames: _split(map['readonly']),
       showSectionNames: _split(map['show']),
       unfoldSectionNames: _split(map['unfold']),
@@ -54,8 +52,6 @@ class ExampleViewOptions with EquatableMixin {
   }
 
   static const empty = ExampleViewOptions(
-    foldCommentAtLineZero: true,
-    foldImports: true,
     readOnlySectionNames: [],
     showSectionNames: [],
     unfoldSectionNames: [],
diff --git 
a/playground/frontend/playground_components/lib/src/repositories/models/shared_file.dart
 b/playground/frontend/playground_components/lib/src/models/snippet_file.dart
similarity index 60%
rename from 
playground/frontend/playground_components/lib/src/repositories/models/shared_file.dart
rename to 
playground/frontend/playground_components/lib/src/models/snippet_file.dart
index 7a63f2a91cd..492eb7ba5c0 100644
--- 
a/playground/frontend/playground_components/lib/src/repositories/models/shared_file.dart
+++ b/playground/frontend/playground_components/lib/src/models/snippet_file.dart
@@ -16,14 +16,37 @@
  * limitations under the License.
  */
 
-class SharedFile {
-  final String code;
+import 'package:equatable/equatable.dart';
+import 'package:json_annotation/json_annotation.dart';
+
+part 'snippet_file.g.dart';
+
+@JsonSerializable()
+class SnippetFile with EquatableMixin {
+  final String content;
   final bool isMain;
   final String name;
 
-  const SharedFile({
-    required this.code,
+  const SnippetFile({
+    required this.content,
     required this.isMain,
     this.name = '',
   });
+
+  static const empty = SnippetFile(
+    content: '',
+    isMain: true,
+  );
+
+  Map<String, dynamic> toJson() => _$SnippetFileToJson(this);
+
+  factory SnippetFile.fromJson(Map<String, dynamic> map) =>
+      _$SnippetFileFromJson(map);
+
+  @override
+  List<Object?> get props => [
+        content,
+        isMain,
+        name,
+      ];
 }
diff --git 
a/playground/frontend/playground_components/lib/src/models/snippet_file.g.dart 
b/playground/frontend/playground_components/lib/src/models/snippet_file.g.dart
new file mode 100644
index 00000000000..1d492e0ffb1
--- /dev/null
+++ 
b/playground/frontend/playground_components/lib/src/models/snippet_file.g.dart
@@ -0,0 +1,20 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'snippet_file.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+SnippetFile _$SnippetFileFromJson(Map<String, dynamic> json) => SnippetFile(
+      content: json['content'] as String,
+      isMain: json['isMain'] as bool,
+      name: json['name'] as String? ?? '',
+    );
+
+Map<String, dynamic> _$SnippetFileToJson(SnippetFile instance) =>
+    <String, dynamic>{
+      'content': instance.content,
+      'isMain': instance.isMain,
+      'name': instance.name,
+    };
diff --git 
a/playground/frontend/playground_components/lib/src/repositories/code_client/grpc_code_client.dart
 
b/playground/frontend/playground_components/lib/src/repositories/code_client/grpc_code_client.dart
index a2ebdf8c147..bfeb517966e 100644
--- 
a/playground/frontend/playground_components/lib/src/repositories/code_client/grpc_code_client.dart
+++ 
b/playground/frontend/playground_components/lib/src/repositories/code_client/grpc_code_client.dart
@@ -29,6 +29,7 @@ import '../models/run_code_error.dart';
 import '../models/run_code_request.dart';
 import '../models/run_code_response.dart';
 import '../models/run_code_result.dart';
+import '../models/snippet_file_grpc_extension.dart';
 import '../sdk_grpc_extension.dart';
 import 'code_client.dart';
 
@@ -215,10 +216,10 @@ class GrpcCodeClient implements CodeClient {
 
   grpc.RunCodeRequest _grpcRunCodeRequest(RunCodeRequest request) {
     return grpc.RunCodeRequest(
-      code: request.code,
-      sdk: request.sdk.grpc,
-      pipelineOptions: pipelineOptionsToString(request.pipelineOptions),
       datasets: request.datasets.map((e) => e.grpc),
+      files: request.files.map((f) => f.grpc),
+      pipelineOptions: pipelineOptionsToString(request.pipelineOptions),
+      sdk: request.sdk.grpc,
     );
   }
 
diff --git 
a/playground/frontend/playground_components/lib/src/repositories/example_client/grpc_example_client.dart
 
b/playground/frontend/playground_components/lib/src/repositories/example_client/grpc_example_client.dart
index 8df2a6b4204..e4ce41ec824 100644
--- 
a/playground/frontend/playground_components/lib/src/repositories/example_client/grpc_example_client.dart
+++ 
b/playground/frontend/playground_components/lib/src/repositories/example_client/grpc_example_client.dart
@@ -23,6 +23,7 @@ import '../../api/v1/api.pbgrpc.dart' as grpc;
 import '../../models/category_with_examples.dart';
 import '../../models/example_base.dart';
 import '../../models/sdk.dart';
+import '../../models/snippet_file.dart';
 import '../complexity_grpc_extension.dart';
 import '../dataset_grpc_extension.dart';
 import '../models/get_default_precompiled_object_request.dart';
@@ -36,7 +37,7 @@ import '../models/get_snippet_response.dart';
 import '../models/output_response.dart';
 import '../models/save_snippet_request.dart';
 import '../models/save_snippet_response.dart';
-import '../models/shared_file.dart';
+import '../models/snippet_file_grpc_extension.dart';
 import '../sdk_grpc_extension.dart';
 import 'example_client.dart';
 
@@ -119,7 +120,9 @@ class GrpcExampleClient implements ExampleClient {
       ),
     );
 
-    return GetPrecompiledObjectCodeResponse(code: response.code);
+    return GetPrecompiledObjectCodeResponse(
+      files: response.files.map((f) => f.model).toList(growable: false),
+    );
   }
 
   @override
@@ -340,33 +343,36 @@ class GrpcExampleClient implements ExampleClient {
     );
   }
 
-  List<SharedFile> _convertToSharedFileList(
+  List<SnippetFile> _convertToSharedFileList(
     List<grpc.SnippetFile> snippetFileList,
   ) {
-    final sharedFilesList = <SharedFile>[];
+    final sharedFilesList = <SnippetFile>[];
 
     for (final item in snippetFileList) {
-      sharedFilesList.add(SharedFile(
-        code: item.content,
-        isMain: item.isMain,
-        name: item.name,
-      ));
+      sharedFilesList.add(
+        SnippetFile(
+          content: item.content,
+          isMain: item.isMain,
+          name: item.name,
+        ),
+      );
     }
 
     return sharedFilesList;
   }
 
   List<grpc.SnippetFile> _convertToSnippetFileList(
-    List<SharedFile> sharedFilesList,
+    List<SnippetFile> sharedFilesList,
   ) {
     final snippetFileList = <grpc.SnippetFile>[];
 
     for (final item in sharedFilesList) {
       snippetFileList.add(
-        grpc.SnippetFile()
-          ..name = item.name
-          ..isMain = true
-          ..content = item.code,
+        grpc.SnippetFile(
+          content: item.content,
+          isMain: true,
+          name: item.name,
+        ),
       );
     }
 
diff --git 
a/playground/frontend/playground_components/lib/src/repositories/example_repository.dart
 
b/playground/frontend/playground_components/lib/src/repositories/example_repository.dart
index 1319ff52f9c..bb35f0c7674 100644
--- 
a/playground/frontend/playground_components/lib/src/repositories/example_repository.dart
+++ 
b/playground/frontend/playground_components/lib/src/repositories/example_repository.dart
@@ -19,6 +19,7 @@
 import '../models/category_with_examples.dart';
 import '../models/example_base.dart';
 import '../models/sdk.dart';
+import '../models/snippet_file.dart';
 import 'example_client/example_client.dart';
 import 'models/get_default_precompiled_object_request.dart';
 import 'models/get_precompiled_object_request.dart';
@@ -48,11 +49,11 @@ class ExampleRepository {
     return result.example;
   }
 
-  Future<String> getPrecompiledObjectCode(
+  Future<List<SnippetFile>> getPrecompiledObjectCode(
     GetPrecompiledObjectRequest request,
   ) async {
     final result = await _client.getPrecompiledObjectCode(request);
-    return result.code;
+    return result.files;
   }
 
   Future<String> getPrecompiledObjectOutput(
diff --git 
a/playground/frontend/playground_components/lib/src/repositories/models/get_precompiled_object_code_response.dart
 
b/playground/frontend/playground_components/lib/src/repositories/models/get_precompiled_object_code_response.dart
index a7bbda2459d..0f1895972c6 100644
--- 
a/playground/frontend/playground_components/lib/src/repositories/models/get_precompiled_object_code_response.dart
+++ 
b/playground/frontend/playground_components/lib/src/repositories/models/get_precompiled_object_code_response.dart
@@ -16,10 +16,12 @@
  * limitations under the License.
  */
 
+import '../../models/snippet_file.dart';
+
 class GetPrecompiledObjectCodeResponse {
-  final String code;
+  final List<SnippetFile> files;
 
   const GetPrecompiledObjectCodeResponse({
-    required this.code,
+    required this.files,
   });
 }
diff --git 
a/playground/frontend/playground_components/lib/src/repositories/models/get_snippet_response.dart
 
b/playground/frontend/playground_components/lib/src/repositories/models/get_snippet_response.dart
index c935936898d..17e1a92673e 100644
--- 
a/playground/frontend/playground_components/lib/src/repositories/models/get_snippet_response.dart
+++ 
b/playground/frontend/playground_components/lib/src/repositories/models/get_snippet_response.dart
@@ -18,11 +18,11 @@
 
 import '../../enums/complexity.dart';
 import '../../models/sdk.dart';
-import 'shared_file.dart';
+import '../../models/snippet_file.dart';
 
 class GetSnippetResponse {
   final Complexity? complexity;
-  final List<SharedFile> files;
+  final List<SnippetFile> files;
   final String pipelineOptions;
   final Sdk sdk;
 
diff --git 
a/playground/frontend/playground_components/lib/src/repositories/models/run_code_request.dart
 
b/playground/frontend/playground_components/lib/src/repositories/models/run_code_request.dart
index d2a49b9ee6d..ddfd695dc39 100644
--- 
a/playground/frontend/playground_components/lib/src/repositories/models/run_code_request.dart
+++ 
b/playground/frontend/playground_components/lib/src/repositories/models/run_code_request.dart
@@ -18,17 +18,18 @@
 
 import '../../models/dataset.dart';
 import '../../models/sdk.dart';
+import '../../models/snippet_file.dart';
 
 class RunCodeRequest {
-  final String code;
   final List<Dataset> datasets;
-  final Sdk sdk;
+  final List<SnippetFile> files;
   final Map<String, String> pipelineOptions;
+  final Sdk sdk;
 
   const RunCodeRequest({
-    required this.code,
     required this.datasets,
-    required this.sdk,
+    required this.files,
     required this.pipelineOptions,
+    required this.sdk,
   });
 }
diff --git 
a/playground/frontend/playground_components/lib/src/repositories/models/save_snippet_request.dart
 
b/playground/frontend/playground_components/lib/src/repositories/models/save_snippet_request.dart
index 4d64416efcc..c2043a754aa 100644
--- 
a/playground/frontend/playground_components/lib/src/repositories/models/save_snippet_request.dart
+++ 
b/playground/frontend/playground_components/lib/src/repositories/models/save_snippet_request.dart
@@ -17,10 +17,10 @@
  */
 
 import '../../models/sdk.dart';
-import 'shared_file.dart';
+import '../../models/snippet_file.dart';
 
 class SaveSnippetRequest {
-  final List<SharedFile> files;
+  final List<SnippetFile> files;
   final Sdk sdk;
   final String pipelineOptions;
 
diff --git 
a/playground/frontend/playground_components/lib/src/repositories/models/get_snippet_response.dart
 
b/playground/frontend/playground_components/lib/src/repositories/models/snippet_file_grpc_extension.dart
similarity index 65%
copy from 
playground/frontend/playground_components/lib/src/repositories/models/get_snippet_response.dart
copy to 
playground/frontend/playground_components/lib/src/repositories/models/snippet_file_grpc_extension.dart
index c935936898d..e4b46ba42c0 100644
--- 
a/playground/frontend/playground_components/lib/src/repositories/models/get_snippet_response.dart
+++ 
b/playground/frontend/playground_components/lib/src/repositories/models/snippet_file_grpc_extension.dart
@@ -16,20 +16,21 @@
  * limitations under the License.
  */
 
-import '../../enums/complexity.dart';
-import '../../models/sdk.dart';
-import 'shared_file.dart';
+import '../../api/v1/api.pb.dart' as g;
+import '../../models/snippet_file.dart';
 
-class GetSnippetResponse {
-  final Complexity? complexity;
-  final List<SharedFile> files;
-  final String pipelineOptions;
-  final Sdk sdk;
+extension SnippetFileExtension on SnippetFile {
+  g.SnippetFile get grpc => g.SnippetFile(
+        content: content,
+        isMain: isMain,
+        name: name,
+      );
+}
 
-  const GetSnippetResponse({
-    required this.complexity,
-    required this.files,
-    required this.pipelineOptions,
-    required this.sdk,
-  });
+extension SnippetFileGrpcExtension on g.SnippetFile {
+  SnippetFile get model => SnippetFile(
+        content: content,
+        isMain: isMain,
+        name: name,
+      );
 }
diff --git 
a/playground/frontend/playground_components/lib/src/widgets/complexity.dart 
b/playground/frontend/playground_components/lib/src/widgets/complexity.dart
index 813d905f95d..ce8c7e087cf 100644
--- a/playground/frontend/playground_components/lib/src/widgets/complexity.dart
+++ b/playground/frontend/playground_components/lib/src/widgets/complexity.dart
@@ -29,7 +29,10 @@ class ComplexityWidget extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return Row(children: _dots[complexity]!);
+    return Row(
+      mainAxisSize: MainAxisSize.min,
+      children: _dots[complexity]!,
+    );
   }
 
   static const Map<Complexity, List<Widget>> _dots = {
diff --git 
a/playground/frontend/playground_components/lib/src/widgets/run_or_cancel_button.dart
 
b/playground/frontend/playground_components/lib/src/widgets/run_or_cancel_button.dart
index 5ec07458401..04b6b911f76 100644
--- 
a/playground/frontend/playground_components/lib/src/widgets/run_or_cancel_button.dart
+++ 
b/playground/frontend/playground_components/lib/src/widgets/run_or_cancel_button.dart
@@ -42,7 +42,6 @@ class RunOrCancelButton extends StatelessWidget {
   Widget build(BuildContext context) {
     return RunButton(
       playgroundController: playgroundController,
-      disabled: playgroundController.selectedExample?.isMultiFile ?? false,
       isRunning: playgroundController.isCodeRunning,
       cancelRun: () {
         beforeCancel?.call();
diff --git 
a/playground/frontend/playground_components/lib/src/widgets/snippet_editor.dart 
b/playground/frontend/playground_components/lib/src/widgets/snippet_editor.dart
index ddcba180b89..9f0442695c2 100644
--- 
a/playground/frontend/playground_components/lib/src/widgets/snippet_editor.dart
+++ 
b/playground/frontend/playground_components/lib/src/widgets/snippet_editor.dart
@@ -16,120 +16,67 @@
  * limitations under the License.
  */
 
-import 'dart:math';
-
 import 'package:flutter/material.dart';
-import 'package:flutter/scheduler.dart';
-import 'package:flutter_code_editor/flutter_code_editor.dart';
 
+import '../constants/sizes.dart';
 import '../controllers/snippet_editing_controller.dart';
-import '../theme/theme.dart';
-
-class SnippetEditor extends StatefulWidget {
-  final SnippetEditingController controller;
-  final bool isEditable;
+import 'loading_indicator.dart';
+import 'snippet_file_editor.dart';
+import 'tabbed_snippet_editor.dart';
 
-  SnippetEditor({
+class SnippetEditor extends StatelessWidget {
+  const SnippetEditor({
     required this.controller,
     required this.isEditable,
-  }) : super(
-    // When the example is changed, will scroll to the context line again.
-    key: ValueKey(controller.selectedExample),
-  );
-
-  @override
-  State<SnippetEditor> createState() => _SnippetEditorState();
-}
-
-class _SnippetEditorState extends State<SnippetEditor> {
-  bool _didAutoFocus = false;
-  final _focusNode = FocusNode();
-  final _scrollController = ScrollController();
-
-  @override
-  void didChangeDependencies() {
-    super.didChangeDependencies();
-
-    if (!_didAutoFocus) {
-      _didAutoFocus = true;
-      SchedulerBinding.instance.addPostFrameCallback((_) {
-        if (mounted) {
-          _scrollSoCursorIsOnTop();
-        }
-      });
-    }
-  }
-
-  void _scrollSoCursorIsOnTop() {
-    _focusNode.requestFocus();
+    this.actionsWidget,
+  });
 
-    final position = max(widget.controller.codeController.selection.start, 0);
-    final characterOffset = _getLastCharacterOffset(
-      text: widget.controller.codeController.text.substring(0, position),
-      style: kLightTheme.extension<BeamThemeExtension>()!.codeRootStyle,
-    );
+  final SnippetEditingController controller;
+  final bool isEditable;
 
-    _scrollController.jumpTo(
-      min(
-        characterOffset.dy,
-        _scrollController.position.maxScrollExtent,
-      ),
-    );
-  }
-
-  @override
-  void dispose() {
-    _focusNode.dispose();
-    super.dispose();
-  }
+  /// A child widget that will be:
+  ///  - Hidden if no file is loaded.
+  ///  - Shown as an overlay for a single file editor.
+  ///  - Built into the tab bar for a multi-file editor.
+  final Widget? actionsWidget;
 
   @override
   Widget build(BuildContext context) {
-    final ext = Theme.of(context).extension<BeamThemeExtension>()!;
-    final isMultiFile = widget.controller.selectedExample?.isMultiFile ?? 
false;
-    final isEnabled = widget.isEditable && !isMultiFile;
-
-    return Semantics(
-      container: true,
-      enabled: isEnabled,
-      label: 'widgets.codeEditor.label',
-      multiline: true,
-      readOnly: isEnabled,
-      textField: true,
-      child: FocusScope(
-        node: FocusScopeNode(canRequestFocus: isEnabled),
-        child: CodeTheme(
-          data: ext.codeTheme,
-          child: Container(
-            color: ext.codeTheme.styles['root']?.backgroundColor,
-            child: SingleChildScrollView(
-              controller: _scrollController,
-              child: CodeField(
-                key: ValueKey(widget.controller.codeController),
-                controller: widget.controller.codeController,
-                enabled: isEnabled,
-                focusNode: _focusNode,
-                textStyle: ext.codeRootStyle,
-              ),
-            ),
-          ),
-        ),
-      ),
+    return AnimatedBuilder(
+      animation: controller,
+      builder: (context, child) {
+        switch (controller.fileControllers.length) {
+          case 0:
+            return const Center(
+              child: LoadingIndicator(),
+            );
+
+          case 1:
+            return Stack(
+              children: [
+                Positioned.fill(
+                  child: SnippetFileEditor(
+                    controller: controller.fileControllers.first,
+                    isEditable: isEditable,
+                  ),
+                ),
+                if (actionsWidget != null)
+                  Positioned(
+                    right: 0,
+                    top: BeamSizes.size10,
+                    child: actionsWidget!,
+                  ),
+              ],
+            );
+
+          default:
+            return TabbedSnippetEditor(
+              controller: controller,
+              isEditable: isEditable,
+              trailing: actionsWidget,
+            );
+        }
+      },
     );
   }
 }
-
-Offset _getLastCharacterOffset({
-  required String text,
-  required TextStyle style,
-}) {
-  final textPainter = TextPainter(
-    textDirection: TextDirection.ltr,
-    text: TextSpan(text: text, style: style),
-  )..layout();
-
-  return textPainter.getOffsetForCaret(
-    TextPosition(offset: text.length),
-    Rect.zero,
-  );
-}
diff --git 
a/playground/frontend/playground_components/lib/src/widgets/snippet_editor.dart 
b/playground/frontend/playground_components/lib/src/widgets/snippet_file_editor.dart
similarity index 82%
copy from 
playground/frontend/playground_components/lib/src/widgets/snippet_editor.dart
copy to 
playground/frontend/playground_components/lib/src/widgets/snippet_file_editor.dart
index ddcba180b89..85b01027651 100644
--- 
a/playground/frontend/playground_components/lib/src/widgets/snippet_editor.dart
+++ 
b/playground/frontend/playground_components/lib/src/widgets/snippet_file_editor.dart
@@ -22,30 +22,31 @@ import 'package:flutter/material.dart';
 import 'package:flutter/scheduler.dart';
 import 'package:flutter_code_editor/flutter_code_editor.dart';
 
-import '../controllers/snippet_editing_controller.dart';
+import '../controllers/snippet_file_editing_controller.dart';
 import '../theme/theme.dart';
 
-class SnippetEditor extends StatefulWidget {
-  final SnippetEditingController controller;
-  final bool isEditable;
-
-  SnippetEditor({
+class SnippetFileEditor extends StatefulWidget {
+  SnippetFileEditor({
     required this.controller,
     required this.isEditable,
   }) : super(
-    // When the example is changed, will scroll to the context line again.
-    key: ValueKey(controller.selectedExample),
-  );
+          // When the example is changed, will scroll to the context line 
again.
+          key: ValueKey(controller.savedFile),
+        );
+
+  final SnippetFileEditingController controller;
+  final bool isEditable;
 
   @override
-  State<SnippetEditor> createState() => _SnippetEditorState();
+  State<SnippetFileEditor> createState() => _SnippetFileEditorState();
 }
 
-class _SnippetEditorState extends State<SnippetEditor> {
+class _SnippetFileEditorState extends State<SnippetFileEditor> {
   bool _didAutoFocus = false;
   final _focusNode = FocusNode();
   final _scrollController = ScrollController();
 
+
   @override
   void didChangeDependencies() {
     super.didChangeDependencies();
@@ -65,8 +66,8 @@ class _SnippetEditorState extends State<SnippetEditor> {
 
     final position = max(widget.controller.codeController.selection.start, 0);
     final characterOffset = _getLastCharacterOffset(
-      text: widget.controller.codeController.text.substring(0, position),
       style: kLightTheme.extension<BeamThemeExtension>()!.codeRootStyle,
+      text: widget.controller.codeController.text.substring(0, position),
     );
 
     _scrollController.jumpTo(
@@ -86,18 +87,16 @@ class _SnippetEditorState extends State<SnippetEditor> {
   @override
   Widget build(BuildContext context) {
     final ext = Theme.of(context).extension<BeamThemeExtension>()!;
-    final isMultiFile = widget.controller.selectedExample?.isMultiFile ?? 
false;
-    final isEnabled = widget.isEditable && !isMultiFile;
 
     return Semantics(
       container: true,
-      enabled: isEnabled,
+      enabled: widget.isEditable,
       label: 'widgets.codeEditor.label',
       multiline: true,
-      readOnly: isEnabled,
+      readOnly: !widget.isEditable,
       textField: true,
       child: FocusScope(
-        node: FocusScopeNode(canRequestFocus: isEnabled),
+        node: FocusScopeNode(canRequestFocus: widget.isEditable),
         child: CodeTheme(
           data: ext.codeTheme,
           child: Container(
@@ -107,7 +106,7 @@ class _SnippetEditorState extends State<SnippetEditor> {
               child: CodeField(
                 key: ValueKey(widget.controller.codeController),
                 controller: widget.controller.codeController,
-                enabled: isEnabled,
+                enabled: widget.isEditable,
                 focusNode: _focusNode,
                 textStyle: ext.codeRootStyle,
               ),
@@ -120,8 +119,8 @@ class _SnippetEditorState extends State<SnippetEditor> {
 }
 
 Offset _getLastCharacterOffset({
-  required String text,
   required TextStyle style,
+  required String text,
 }) {
   final textPainter = TextPainter(
     textDirection: TextDirection.ltr,
diff --git 
a/playground/frontend/playground_components/lib/src/widgets/tab_header.dart 
b/playground/frontend/playground_components/lib/src/widgets/tab_header.dart
index 714f025814e..31d4f132aa7 100644
--- a/playground/frontend/playground_components/lib/src/widgets/tab_header.dart
+++ b/playground/frontend/playground_components/lib/src/widgets/tab_header.dart
@@ -20,8 +20,6 @@ import 'package:flutter/material.dart';
 
 import '../constants/sizes.dart';
 
-const kHeaderHeight = 50.0;
-
 class TabHeader extends StatelessWidget {
   final TabController tabController;
   final Widget tabsWidget;
@@ -35,7 +33,7 @@ class TabHeader extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return SizedBox(
-      height: 50,
+      height: BeamSizes.tabBarHeight,
       child: Padding(
         padding: const EdgeInsets.symmetric(
           horizontal: BeamSizes.size16,
diff --git 
a/playground/frontend/playground_components/lib/src/widgets/tabbed_snippet_editor.dart
 
b/playground/frontend/playground_components/lib/src/widgets/tabbed_snippet_editor.dart
new file mode 100644
index 00000000000..d834f82a800
--- /dev/null
+++ 
b/playground/frontend/playground_components/lib/src/widgets/tabbed_snippet_editor.dart
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'package:collection/collection.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:keyed_collection_widgets/keyed_collection_widgets.dart';
+
+import '../controllers/snippet_editing_controller.dart';
+import 'snippet_file_editor.dart';
+import 'tabs/tab_bar.dart';
+
+class TabbedSnippetEditor extends StatelessWidget {
+  const TabbedSnippetEditor({
+    required this.controller,
+    required this.isEditable,
+    this.trailing,
+  });
+
+  final SnippetEditingController controller;
+  final bool isEditable;
+  final Widget? trailing;
+
+  @override
+  Widget build(BuildContext context) {
+    final files = controller.fileControllers.map((c) => c.getFile());
+    final keys = files.map((f) => f.name).toList(growable: false);
+    final initialKey = files.firstWhereOrNull((f) => f.isMain)?.name;
+
+    return DefaultKeyedTabController<String>.fromKeys(
+      animationDuration: Duration.zero,
+      initialKey: initialKey,
+      keys: keys,
+      onChanged: (key) {
+        if (key != null) {
+          controller.activateFileControllerByName(key);
+        }
+      },
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Row(
+            children: [
+              Expanded(
+                child:
+                    BeamTabBar(tabs: {for (final key in keys) key: Text(key)}),
+              ),
+              if (trailing != null) trailing!,
+            ],
+          ),
+          Expanded(
+            child: KeyedTabBarView.withDefaultController(
+              children: {
+                for (final key in keys)
+                  key: SnippetFileEditor(
+                    controller: controller.getFileControllerByName(key)!,
+                    isEditable: isEditable,
+                  ),
+              },
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git 
a/playground/frontend/playground_components/lib/src/widgets/tab_header.dart 
b/playground/frontend/playground_components/lib/src/widgets/tabs/tab_bar.dart
similarity index 68%
copy from 
playground/frontend/playground_components/lib/src/widgets/tab_header.dart
copy to 
playground/frontend/playground_components/lib/src/widgets/tabs/tab_bar.dart
index 714f025814e..7c4a864f7b3 100644
--- a/playground/frontend/playground_components/lib/src/widgets/tab_header.dart
+++ 
b/playground/frontend/playground_components/lib/src/widgets/tabs/tab_bar.dart
@@ -17,30 +17,25 @@
  */
 
 import 'package:flutter/material.dart';
+import 'package:keyed_collection_widgets/keyed_collection_widgets.dart';
 
-import '../constants/sizes.dart';
+import '../../constants/sizes.dart';
 
-const kHeaderHeight = 50.0;
-
-class TabHeader extends StatelessWidget {
-  final TabController tabController;
-  final Widget tabsWidget;
-
-  const TabHeader({
+class BeamTabBar<K extends Object> extends StatelessWidget {
+  const BeamTabBar({
     super.key,
-    required this.tabController,
-    required this.tabsWidget,
+    required this.tabs,
   });
 
+  final Map<K, Widget> tabs;
+
   @override
   Widget build(BuildContext context) {
     return SizedBox(
-      height: 50,
-      child: Padding(
-        padding: const EdgeInsets.symmetric(
-          horizontal: BeamSizes.size16,
-        ),
-        child: tabsWidget,
+      height: BeamSizes.tabBarHeight,
+      child: KeyedTabBar.withDefaultController<K>(
+        isScrollable: true,
+        tabs: {for (final key in tabs.keys) key: Tab(child: tabs[key])},
       ),
     );
   }
diff --git a/playground/frontend/playground_components/pubspec.yaml 
b/playground/frontend/playground_components/pubspec.yaml
index e7c2943c9b7..a87ac0f2a23 100644
--- a/playground/frontend/playground_components/pubspec.yaml
+++ b/playground/frontend/playground_components/pubspec.yaml
@@ -34,7 +34,7 @@ dependencies:
   enum_map: ^0.2.1
   equatable: ^2.0.5
   flutter: { sdk: flutter }
-  flutter_code_editor: ^0.2.4
+  flutter_code_editor: ^0.2.5
   flutter_markdown: ^0.6.12
   flutter_svg: ^1.0.3
   fluttertoast: ^8.1.1
@@ -44,6 +44,7 @@ dependencies:
   highlight: ^0.7.0
   http: ^0.13.5
   json_annotation: ^4.7.0
+  keyed_collection_widgets: ^0.4.3
   meta: ^1.7.0
   protobuf: ^2.1.0
   provider: ^6.0.3
diff --git 
a/playground/frontend/playground_components/test/src/cache/example_cache_test.dart
 
b/playground/frontend/playground_components/test/src/cache/example_cache_test.dart
index 38781e1b719..a05785a6681 100644
--- 
a/playground/frontend/playground_components/test/src/cache/example_cache_test.dart
+++ 
b/playground/frontend/playground_components/test/src/cache/example_cache_test.dart
@@ -29,11 +29,11 @@ import '../common/example_repository_mock.mocks.dart';
 import '../common/examples.dart';
 import '../common/requests.dart';
 
-final kDefaultExamplesMapMock = UnmodifiableMapView({
-  Sdk.java: exampleWithAllAdditionsMock,
-  Sdk.go: exampleWithAllAdditionsMock,
-  Sdk.python: exampleWithAllAdditionsMock,
-  Sdk.scio: exampleWithAllAdditionsMock,
+final _defaultExamplesMapMock = UnmodifiableMapView({
+  Sdk.java: examplePython3,
+  Sdk.go: examplePython3,
+  Sdk.python: examplePython3,
+  Sdk.scio: examplePython3,
 });
 
 void main() {
@@ -75,8 +75,8 @@ void main() {
         'then loadExampleInfo should return example immediately',
         () async {
           expect(
-            await cache.loadExampleInfo(exampleMock1),
-            exampleMock1,
+            await cache.loadExampleInfo(examplePython1),
+            examplePython1,
           );
         },
       );
@@ -84,9 +84,18 @@ void main() {
       test(
         'loadExampleInfo loads source, output, logs, graph for given example',
         () async {
+          when(mockRepo.getPrecompiledObjectOutput(kRequestForExampleInfo))
+              .thenAnswer((_) async => examplePython3.outputs!);
+          when(mockRepo.getPrecompiledObjectCode(kRequestForExampleInfo))
+              .thenAnswer((_) async => examplePython3.files);
+          when(mockRepo.getPrecompiledObjectLogs(kRequestForExampleInfo))
+              .thenAnswer((_) async => examplePython3.logs!);
+          when(mockRepo.getPrecompiledObjectGraph(kRequestForExampleInfo))
+              .thenAnswer((_) async => examplePython3.graph!);
+
           expect(
-            await cache.loadExampleInfo(exampleWithoutSourceMock),
-            exampleWithAllAdditionsMock,
+            await cache.loadExampleInfo(exampleBasePython3),
+            examplePython3,
           );
         },
       );
@@ -97,9 +106,9 @@ void main() {
         'If defaultExamplesBySdk is not empty, '
         'loadDefaultExamples should not change it',
         () async {
-          cache.defaultExamplesBySdk.addAll(kDefaultExamplesMapMock);
+          cache.defaultExamplesBySdk.addAll(_defaultExamplesMapMock);
           await cache.loadDefaultPrecompiledObjects();
-          expect(cache.defaultExamplesBySdk, kDefaultExamplesMapMock);
+          expect(cache.defaultExamplesBySdk, _defaultExamplesMapMock);
         },
       );
 
@@ -109,19 +118,19 @@ void main() {
         () async {
           // stubs
           when(mockRepo.getPrecompiledObjectOutput(kRequestForExampleInfo))
-              .thenAnswer((_) async => kOutputResponse.output);
+              .thenAnswer((_) async => examplePython3.outputs!);
           when(mockRepo.getPrecompiledObjectCode(kRequestForExampleInfo))
-              .thenAnswer((_) async => kOutputResponse.output);
+              .thenAnswer((_) async => examplePython3.files);
           when(mockRepo.getPrecompiledObjectLogs(kRequestForExampleInfo))
-              .thenAnswer((_) async => kOutputResponse.output);
+              .thenAnswer((_) async => examplePython3.logs!);
           when(mockRepo.getPrecompiledObjectGraph(kRequestForExampleInfo))
-              .thenAnswer((_) async => kOutputResponse.output);
+              .thenAnswer((_) async => examplePython3.graph!);
 
           // test assertion
           await cache.loadDefaultPrecompiledObjects();
           expect(
             cache.defaultExamplesBySdk,
-            kDefaultExamplesMapMock,
+            _defaultExamplesMapMock,
           );
         },
       );
diff --git 
a/playground/frontend/playground_components/test/src/common/categories.dart 
b/playground/frontend/playground_components/test/src/common/categories.dart
index 2bba911d19c..07a2acfa508 100644
--- a/playground/frontend/playground_components/test/src/common/categories.dart
+++ b/playground/frontend/playground_components/test/src/common/categories.dart
@@ -24,25 +24,25 @@ import 'package:playground_components/src/models/sdk.dart';
 import 'examples.dart';
 
 final categoriesMock = [
-  CategoryWithExamples(title: 'Filtered', examples: [exampleMock1]),
+  CategoryWithExamples(title: 'Filtered', examples: [examplePython1]),
   CategoryWithExamples(
     title: 'Unfiltered',
     // exampleMock2 is repeated to test that 'tag2' is more frequent than 
'tag1'
-    examples: [exampleMock1, exampleMock2, exampleMock2, exampleMock2],
+    examples: [examplePython1, examplePython2, examplePython2, examplePython2],
   ),
 ];
 
 final filteredCategories = [
-  CategoryWithExamples(title: 'Filtered', examples: [exampleMock1]),
+  CategoryWithExamples(title: 'Filtered', examples: [examplePython1]),
 ];
 
-const filteredExamples = [exampleMock1, exampleMock2];
+const filteredExamples = [examplePython1, examplePython2];
 
-const examplesFilteredByTypeMock = [exampleMock2];
+const examplesFilteredByTypeMock = [examplePython2];
 
-const examplesFilteredByTagsMock = [exampleMock2];
+const examplesFilteredByTagsMock = [examplePython2];
 
-const examplesFilteredByNameMock = [exampleMock1];
+const examplesFilteredByNameMock = [examplePython1];
 
 final sdkCategoriesFromServerMock = UnmodifiableMapView({
   Sdk.java: categoriesMock,
diff --git 
a/playground/frontend/playground_components/test/src/common/descriptors.dart 
b/playground/frontend/playground_components/test/src/common/descriptors.dart
index ad2259f47ce..6d65c9d591a 100644
--- a/playground/frontend/playground_components/test/src/common/descriptors.dart
+++ b/playground/frontend/playground_components/test/src/common/descriptors.dart
@@ -23,16 +23,16 @@ import 'examples.dart';
 const emptyDescriptor = EmptyExampleLoadingDescriptor(sdk: Sdk.java);
 
 final standardDescriptor1 = StandardExampleLoadingDescriptor(
-  path: exampleMock1.path,
-  sdk: exampleMock1.sdk,
+  path: examplePython1.path,
+  sdk: examplePython1.sdk,
 );
 
 final standardDescriptor2 = StandardExampleLoadingDescriptor(
-  path: exampleMock2.path,
-  sdk: exampleMock2.sdk,
+  path: examplePython2.path,
+  sdk: examplePython2.sdk,
 );
 
 final standardGoDescriptor = StandardExampleLoadingDescriptor(
-  path: exampleMockGo.path,
-  sdk: exampleMockGo.sdk,
+  path: exampleGo6.path,
+  sdk: exampleGo6.sdk,
 );
diff --git 
a/playground/frontend/playground_components/test/src/common/example_cache.mocks.dart
 
b/playground/frontend/playground_components/test/src/common/example_cache.mocks.dart
index ccd67c752ba..5ce9a48c2f3 100644
--- 
a/playground/frontend/playground_components/test/src/common/example_cache.mocks.dart
+++ 
b/playground/frontend/playground_components/test/src/common/example_cache.mocks.dart
@@ -14,8 +14,7 @@ import 
'package:playground_components/src/models/example.dart' as _i3;
 import 'package:playground_components/src/models/example_base.dart' as _i2;
 import 'package:playground_components/src/models/loading_status.dart' as _i8;
 import 'package:playground_components/src/models/sdk.dart' as _i5;
-import 'package:playground_components/src/repositories/models/shared_file.dart'
-    as _i9;
+import 'package:playground_components/src/models/snippet_file.dart' as _i9;
 
 // ignore_for_file: type=lint
 // ignore_for_file: avoid_redundant_argument_values
@@ -121,7 +120,7 @@ class MockExampleCache extends _i1.Mock implements 
_i4.ExampleCache {
       ) as _i7.Future<_i3.Example>);
   @override
   _i7.Future<String> saveSnippet({
-    List<_i9.SharedFile>? files,
+    List<_i9.SnippetFile>? files,
     _i5.Sdk? sdk,
     String? pipelineOptions,
   }) =>
diff --git 
a/playground/frontend/playground_components/test/src/common/example_repository_mock.dart
 
b/playground/frontend/playground_components/test/src/common/example_repository_mock.dart
index 814651db4fa..5386e52144b 100644
--- 
a/playground/frontend/playground_components/test/src/common/example_repository_mock.dart
+++ 
b/playground/frontend/playground_components/test/src/common/example_repository_mock.dart
@@ -30,22 +30,13 @@ MockExampleRepository getMockExampleRepository() {
 
   // stubs
   when(m.getDefaultPrecompiledObject(kRequestDefaultExampleForJava))
-      .thenAnswer((_) async => exampleWithoutSourceMock);
+      .thenAnswer((_) async => exampleBasePython3);
   when(m.getDefaultPrecompiledObject(kRequestDefaultExampleForGo))
-      .thenAnswer((_) async => exampleWithoutSourceMock);
+      .thenAnswer((_) async => exampleBasePython3);
   when(m.getDefaultPrecompiledObject(kRequestDefaultExampleForPython))
-      .thenAnswer((_) async => exampleWithoutSourceMock);
+      .thenAnswer((_) async => exampleBasePython3);
   when(m.getDefaultPrecompiledObject(kRequestDefaultExampleForScio))
-      .thenAnswer((_) async => exampleWithoutSourceMock);
-
-  when(m.getPrecompiledObjectOutput(kRequestForExampleInfo))
-      .thenAnswer((_) async => kOutputResponse.output);
-  when(m.getPrecompiledObjectCode(kRequestForExampleInfo))
-      .thenAnswer((_) async => kOutputResponse.output);
-  when(m.getPrecompiledObjectLogs(kRequestForExampleInfo))
-      .thenAnswer((_) async => kOutputResponse.output);
-  when(m.getPrecompiledObjectGraph(kRequestForExampleInfo))
-      .thenAnswer((_) async => kOutputResponse.output);
+      .thenAnswer((_) async => exampleBasePython3);
 
   return m;
 }
diff --git 
a/playground/frontend/playground_components/test/src/common/example_repository_mock.mocks.dart
 
b/playground/frontend/playground_components/test/src/common/example_repository_mock.mocks.dart
index aef3e11b804..aa55c5193fb 100644
--- 
a/playground/frontend/playground_components/test/src/common/example_repository_mock.mocks.dart
+++ 
b/playground/frontend/playground_components/test/src/common/example_repository_mock.mocks.dart
@@ -10,20 +10,21 @@ import 
'package:playground_components/src/models/category_with_examples.dart'
     as _i7;
 import 'package:playground_components/src/models/example_base.dart' as _i2;
 import 'package:playground_components/src/models/sdk.dart' as _i6;
+import 'package:playground_components/src/models/snippet_file.dart' as _i10;
 import 'package:playground_components/src/repositories/example_repository.dart'
     as _i4;
 import 
'package:playground_components/src/repositories/models/get_default_precompiled_object_request.dart'
     as _i9;
 import 
'package:playground_components/src/repositories/models/get_precompiled_object_request.dart'
-    as _i10;
+    as _i11;
 import 
'package:playground_components/src/repositories/models/get_precompiled_objects_request.dart'
     as _i8;
 import 
'package:playground_components/src/repositories/models/get_snippet_request.dart'
-    as _i11;
+    as _i12;
 import 
'package:playground_components/src/repositories/models/get_snippet_response.dart'
     as _i3;
 import 
'package:playground_components/src/repositories/models/save_snippet_request.dart'
-    as _i12;
+    as _i13;
 
 // ignore_for_file: type=lint
 // ignore_for_file: avoid_redundant_argument_values
@@ -71,18 +72,18 @@ class MockExampleRepository extends _i1.Mock implements 
_i4.ExampleRepository {
         returnValue: Future<_i2.ExampleBase>.value(_FakeExampleBase_0()),
       ) as _i5.Future<_i2.ExampleBase>);
   @override
-  _i5.Future<String> getPrecompiledObjectCode(
-          _i10.GetPrecompiledObjectRequest? request) =>
+  _i5.Future<List<_i10.SnippetFile>> getPrecompiledObjectCode(
+          _i11.GetPrecompiledObjectRequest? request) =>
       (super.noSuchMethod(
         Invocation.method(
           #getPrecompiledObjectCode,
           [request],
         ),
-        returnValue: Future<String>.value(''),
-      ) as _i5.Future<String>);
+        returnValue: 
Future<List<_i10.SnippetFile>>.value(<_i10.SnippetFile>[]),
+      ) as _i5.Future<List<_i10.SnippetFile>>);
   @override
   _i5.Future<String> getPrecompiledObjectOutput(
-          _i10.GetPrecompiledObjectRequest? request) =>
+          _i11.GetPrecompiledObjectRequest? request) =>
       (super.noSuchMethod(
         Invocation.method(
           #getPrecompiledObjectOutput,
@@ -92,7 +93,7 @@ class MockExampleRepository extends _i1.Mock implements 
_i4.ExampleRepository {
       ) as _i5.Future<String>);
   @override
   _i5.Future<String> getPrecompiledObjectLogs(
-          _i10.GetPrecompiledObjectRequest? request) =>
+          _i11.GetPrecompiledObjectRequest? request) =>
       (super.noSuchMethod(
         Invocation.method(
           #getPrecompiledObjectLogs,
@@ -102,7 +103,7 @@ class MockExampleRepository extends _i1.Mock implements 
_i4.ExampleRepository {
       ) as _i5.Future<String>);
   @override
   _i5.Future<String> getPrecompiledObjectGraph(
-          _i10.GetPrecompiledObjectRequest? request) =>
+          _i11.GetPrecompiledObjectRequest? request) =>
       (super.noSuchMethod(
         Invocation.method(
           #getPrecompiledObjectGraph,
@@ -112,7 +113,7 @@ class MockExampleRepository extends _i1.Mock implements 
_i4.ExampleRepository {
       ) as _i5.Future<String>);
   @override
   _i5.Future<_i2.ExampleBase> getPrecompiledObject(
-          _i10.GetPrecompiledObjectRequest? request) =>
+          _i11.GetPrecompiledObjectRequest? request) =>
       (super.noSuchMethod(
         Invocation.method(
           #getPrecompiledObject,
@@ -122,7 +123,7 @@ class MockExampleRepository extends _i1.Mock implements 
_i4.ExampleRepository {
       ) as _i5.Future<_i2.ExampleBase>);
   @override
   _i5.Future<_i3.GetSnippetResponse> getSnippet(
-          _i11.GetSnippetRequest? request) =>
+          _i12.GetSnippetRequest? request) =>
       (super.noSuchMethod(
         Invocation.method(
           #getSnippet,
@@ -132,7 +133,7 @@ class MockExampleRepository extends _i1.Mock implements 
_i4.ExampleRepository {
             Future<_i3.GetSnippetResponse>.value(_FakeGetSnippetResponse_1()),
       ) as _i5.Future<_i3.GetSnippetResponse>);
   @override
-  _i5.Future<String> saveSnippet(_i12.SaveSnippetRequest? request) =>
+  _i5.Future<String> saveSnippet(_i13.SaveSnippetRequest? request) =>
       (super.noSuchMethod(
         Invocation.method(
           #saveSnippet,
diff --git 
a/playground/frontend/playground_components/test/src/common/examples.dart 
b/playground/frontend/playground_components/test/src/common/examples.dart
index 50032f0c112..9a2a5f97b82 100644
--- a/playground/frontend/playground_components/test/src/common/examples.dart
+++ b/playground/frontend/playground_components/test/src/common/examples.dart
@@ -20,30 +20,31 @@ import 
'package:playground_components/src/enums/complexity.dart';
 import 'package:playground_components/src/models/example.dart';
 import 'package:playground_components/src/models/example_base.dart';
 import 'package:playground_components/src/models/sdk.dart';
+import 'package:playground_components/src/models/snippet_file.dart';
 
-const exampleMock1 = Example(
+const examplePython1 = Example(
   complexity: Complexity.basic,
   description: 'description',
+  files: [SnippetFile(content: 'ex1', isMain: true)],
   name: 'Example X1',
   path: 'SDK_PYTHON/Category/Name1',
   sdk: Sdk.python,
-  source: 'ex1',
   tags: ['tag1'],
   type: ExampleType.example,
 );
 
-const exampleMock2 = Example(
+const examplePython2 = Example(
   complexity: Complexity.basic,
   description: 'description',
+  files: [SnippetFile(content: 'ex2', isMain: true)],
   name: 'Kata',
   path: 'SDK_PYTHON/Category/Name2',
   sdk: Sdk.python,
-  source: 'ex2',
   tags: ['tag2'],
   type: ExampleType.kata,
 );
 
-const exampleWithoutSourceMock = ExampleBase(
+const exampleBasePython3 = ExampleBase(
   complexity: Complexity.basic,
   description: 'description',
   name: 'Test example',
@@ -52,38 +53,49 @@ const exampleWithoutSourceMock = ExampleBase(
   type: ExampleType.example,
 );
 
-const exampleWithAllAdditionsMock = Example(
+const examplePython3 = Example(
   complexity: Complexity.basic,
   description: 'description',
-  graph: 'test outputs',
-  logs: 'test outputs',
+  files: [SnippetFile(content: 'test source', isMain: true)],
+  graph: 'test graph',
+  logs: 'test logs',
   name: 'Test example',
   outputs: 'test outputs',
   path: 'SDK_PYTHON/Category/Name',
   sdk: Sdk.python,
-  source: 'test outputs',
   type: ExampleType.example,
 );
 
-const exampleGoPipelineOptions = Example(
+const exampleGo4Multifile = Example(
+  files: [
+    SnippetFile(content: 'go1', isMain: false, name: '1'),
+    SnippetFile(content: 'go2', isMain: true, name: '2'),
+  ],
+  name: 'exampleGo4Multifile',
+  sdk: Sdk.go,
+  type: ExampleType.example,
+  path: 'SDK_GO/Category/exampleGo4Multifile',
+);
+
+const exampleGo5PipelineOptions = Example(
   description: 'description',
-  graph: 'test outputs',
-  logs: 'test outputs',
+  files: [SnippetFile(content: 'test source', isMain: true)],
+  graph: 'test graph',
+  logs: 'test logs',
   name: 'Test example',
   outputs: 'test outputs',
   path: 'SDK_PYTHON/Category/Name',
   pipelineOptions: 'pipeline options',
   sdk: Sdk.go,
-  source: 'test outputs',
   type: ExampleType.example,
 );
 
-const exampleMockGo = Example(
+const exampleGo6 = Example(
   complexity: Complexity.medium,
   description: 'description',
   name: 'Example',
+  files: [SnippetFile(content: 'ex6', isMain: true)],
   path: 'SDK_GO/Category/Name',
   sdk: Sdk.go,
-  source: 'ex1',
   type: ExampleType.example,
 );
diff --git 
a/playground/frontend/playground_components/test/src/common/requests.dart 
b/playground/frontend/playground_components/test/src/common/requests.dart
index 84d015884df..f22014a4011 100644
--- a/playground/frontend/playground_components/test/src/common/requests.dart
+++ b/playground/frontend/playground_components/test/src/common/requests.dart
@@ -17,6 +17,7 @@
  */
 
 import 'package:playground_components/src/models/sdk.dart';
+import 'package:playground_components/src/models/snippet_file.dart';
 import 
'package:playground_components/src/repositories/models/get_default_precompiled_object_request.dart';
 import 
'package:playground_components/src/repositories/models/get_precompiled_object_code_response.dart';
 import 
'package:playground_components/src/repositories/models/get_precompiled_object_request.dart';
@@ -40,11 +41,11 @@ const kGetDefaultPrecompiledObjectRequest = 
GetDefaultPrecompiledObjectRequest(
   sdk: Sdk.java,
 );
 const kGetDefaultPrecompiledObjectResponse = GetPrecompiledObjectResponse(
-  example: exampleMock1,
+  example: examplePython1,
 );
 
 const kGetPrecompiledObjectCodeResponse = GetPrecompiledObjectCodeResponse(
-  code: 'test source',
+  files: [SnippetFile(content: 'test source', isMain: true)],
 );
 const kOutputResponse = OutputResponse(output: 'test outputs');
 
diff --git 
a/playground/frontend/playground_components/test/src/controllers/example_loaders/common.dart
 
b/playground/frontend/playground_components/test/src/controllers/example_loaders/common.dart
index 324ff9c1e00..5015991528d 100644
--- 
a/playground/frontend/playground_components/test/src/controllers/example_loaders/common.dart
+++ 
b/playground/frontend/playground_components/test/src/controllers/example_loaders/common.dart
@@ -46,10 +46,10 @@ class TestExampleLoader extends ExampleLoader {
       : example = descriptor.sdk == null
             ? null
             : Example(
+                files: [SnippetFile(content: descriptor.sdk!.id, isMain: 
true)],
                 name: descriptor.sdk!.id,
                 path: descriptor.sdk!.id,
                 sdk: descriptor.sdk!,
-                source: descriptor.sdk!.id,
                 type: ExampleType.example,
               );
 
diff --git 
a/playground/frontend/playground_components/test/src/controllers/example_loaders/examples_loader_test.mocks.dart
 
b/playground/frontend/playground_components/test/src/controllers/example_loaders/examples_loader_test.mocks.dart
index 7bcc1204ebd..6ac39341a84 100644
--- 
a/playground/frontend/playground_components/test/src/controllers/example_loaders/examples_loader_test.mocks.dart
+++ 
b/playground/frontend/playground_components/test/src/controllers/example_loaders/examples_loader_test.mocks.dart
@@ -3,7 +3,7 @@
 // Do not manually edit this file.
 
 // ignore_for_file: no_leading_underscores_for_library_prefixes
-import 'dart:async' as _i13;
+import 'dart:async' as _i14;
 import 'dart:ui' as _i15;
 
 import 'package:mockito/mockito.dart' as _i1;
@@ -19,7 +19,7 @@ import 
'package:playground_components/src/models/category_with_examples.dart'
 import 'package:playground_components/src/models/example.dart' as _i9;
 import 'package:playground_components/src/models/example_base.dart' as _i8;
 import 
'package:playground_components/src/models/example_loading_descriptors/example_loading_descriptor.dart'
-    as _i14;
+    as _i13;
 import 
'package:playground_components/src/models/example_loading_descriptors/examples_loading_descriptor.dart'
     as _i7;
 import 
'package:playground_components/src/models/example_loading_descriptors/user_shared_example_loading_descriptor.dart'
@@ -28,8 +28,7 @@ import 
'package:playground_components/src/models/loading_status.dart' as _i17;
 import 'package:playground_components/src/models/outputs.dart' as _i11;
 import 'package:playground_components/src/models/sdk.dart' as _i12;
 import 'package:playground_components/src/models/shortcut.dart' as _i4;
-import 'package:playground_components/src/repositories/models/shared_file.dart'
-    as _i18;
+import 'package:playground_components/src/models/snippet_file.dart' as _i18;
 
 // ignore_for_file: type=lint
 // ignore_for_file: avoid_redundant_argument_values
@@ -193,19 +192,9 @@ class MockPlaygroundController extends _i1.Mock
         returnValueForMissingStub: null,
       );
   @override
-  _i13.Future<void> setExampleBase(_i8.ExampleBase? exampleBase) =>
-      (super.noSuchMethod(
-        Invocation.method(
-          #setExampleBase,
-          [exampleBase],
-        ),
-        returnValue: Future<void>.value(),
-        returnValueForMissingStub: Future<void>.value(),
-      ) as _i13.Future<void>);
-  @override
   void setExample(
     _i9.Example? example, {
-    _i14.ExampleLoadingDescriptor? descriptor,
+    _i13.ExampleLoadingDescriptor? descriptor,
     bool? setCurrentSdk,
   }) =>
       super.noSuchMethod(
@@ -233,14 +222,6 @@ class MockPlaygroundController extends _i1.Mock
         returnValueForMissingStub: null,
       );
   @override
-  void setSource(String? source) => super.noSuchMethod(
-        Invocation.method(
-          #setSource,
-          [source],
-        ),
-        returnValueForMissingStub: null,
-      );
-  @override
   void setSelectedOutputFilterType(_i11.OutputType? type) => 
super.noSuchMethod(
         Invocation.method(
           #setSelectedOutputFilterType,
@@ -298,14 +279,14 @@ class MockPlaygroundController extends _i1.Mock
         returnValueForMissingStub: null,
       );
   @override
-  _i13.Future<void> cancelRun() => (super.noSuchMethod(
+  _i14.Future<void> cancelRun() => (super.noSuchMethod(
         Invocation.method(
           #cancelRun,
           [],
         ),
         returnValue: Future<void>.value(),
         returnValueForMissingStub: Future<void>.value(),
-      ) as _i13.Future<void>);
+      ) as _i14.Future<void>);
   @override
   void filterOutput(_i11.OutputType? type) => super.noSuchMethod(
         Invocation.method(
@@ -315,7 +296,7 @@ class MockPlaygroundController extends _i1.Mock
         returnValueForMissingStub: null,
       );
   @override
-  _i13.Future<_i6.UserSharedExampleLoadingDescriptor> saveSnippet() =>
+  _i14.Future<_i6.UserSharedExampleLoadingDescriptor> saveSnippet() =>
       (super.noSuchMethod(
         Invocation.method(
           #saveSnippet,
@@ -323,7 +304,7 @@ class MockPlaygroundController extends _i1.Mock
         ),
         returnValue: Future<_i6.UserSharedExampleLoadingDescriptor>.value(
             _FakeUserSharedExampleLoadingDescriptor_4()),
-      ) as _i13.Future<_i6.UserSharedExampleLoadingDescriptor>);
+      ) as _i14.Future<_i6.UserSharedExampleLoadingDescriptor>);
   @override
   _i7.ExamplesLoadingDescriptor getLoadingDescriptor() => (super.noSuchMethod(
         Invocation.method(
@@ -399,10 +380,10 @@ class MockExampleCache extends _i1.Mock implements 
_i2.ExampleCache {
         returnValueForMissingStub: null,
       );
   @override
-  _i13.Future<void> get allExamplesFuture => (super.noSuchMethod(
+  _i14.Future<void> get allExamplesFuture => (super.noSuchMethod(
         Invocation.getter(#allExamplesFuture),
         returnValue: Future<void>.value(),
-      ) as _i13.Future<void>);
+      ) as _i14.Future<void>);
   @override
   _i17.LoadingStatus get catalogStatus => (super.noSuchMethod(
         Invocation.getter(#catalogStatus),
@@ -414,14 +395,14 @@ class MockExampleCache extends _i1.Mock implements 
_i2.ExampleCache {
         returnValue: false,
       ) as bool);
   @override
-  _i13.Future<void> loadAllPrecompiledObjectsIfNot() => (super.noSuchMethod(
+  _i14.Future<void> loadAllPrecompiledObjectsIfNot() => (super.noSuchMethod(
         Invocation.method(
           #loadAllPrecompiledObjectsIfNot,
           [],
         ),
         returnValue: Future<void>.value(),
         returnValueForMissingStub: Future<void>.value(),
-      ) as _i13.Future<void>);
+      ) as _i14.Future<void>);
   @override
   List<_i16.CategoryWithExamples> getCategories(_i12.Sdk? sdk) =>
       (super.noSuchMethod(
@@ -432,7 +413,7 @@ class MockExampleCache extends _i1.Mock implements 
_i2.ExampleCache {
         returnValue: <_i16.CategoryWithExamples>[],
       ) as List<_i16.CategoryWithExamples>);
   @override
-  _i13.Future<_i8.ExampleBase> getPrecompiledObject(
+  _i14.Future<_i8.ExampleBase> getPrecompiledObject(
     String? path,
     _i12.Sdk? sdk,
   ) =>
@@ -445,18 +426,18 @@ class MockExampleCache extends _i1.Mock implements 
_i2.ExampleCache {
           ],
         ),
         returnValue: Future<_i8.ExampleBase>.value(_FakeExampleBase_6()),
-      ) as _i13.Future<_i8.ExampleBase>);
+      ) as _i14.Future<_i8.ExampleBase>);
   @override
-  _i13.Future<_i9.Example> loadSharedExample(String? id) => 
(super.noSuchMethod(
+  _i14.Future<_i9.Example> loadSharedExample(String? id) => 
(super.noSuchMethod(
         Invocation.method(
           #loadSharedExample,
           [id],
         ),
         returnValue: Future<_i9.Example>.value(_FakeExample_7()),
-      ) as _i13.Future<_i9.Example>);
+      ) as _i14.Future<_i9.Example>);
   @override
-  _i13.Future<String> saveSnippet({
-    List<_i18.SharedFile>? files,
+  _i14.Future<String> saveSnippet({
+    List<_i18.SnippetFile>? files,
     _i12.Sdk? sdk,
     String? pipelineOptions,
   }) =>
@@ -471,16 +452,16 @@ class MockExampleCache extends _i1.Mock implements 
_i2.ExampleCache {
           },
         ),
         returnValue: Future<String>.value(''),
-      ) as _i13.Future<String>);
+      ) as _i14.Future<String>);
   @override
-  _i13.Future<_i9.Example> loadExampleInfo(_i8.ExampleBase? example) =>
+  _i14.Future<_i9.Example> loadExampleInfo(_i8.ExampleBase? example) =>
       (super.noSuchMethod(
         Invocation.method(
           #loadExampleInfo,
           [example],
         ),
         returnValue: Future<_i9.Example>.value(_FakeExample_7()),
-      ) as _i13.Future<_i9.Example>);
+      ) as _i14.Future<_i9.Example>);
   @override
   void setSelectorOpened(bool? value) => super.noSuchMethod(
         Invocation.method(
@@ -490,41 +471,41 @@ class MockExampleCache extends _i1.Mock implements 
_i2.ExampleCache {
         returnValueForMissingStub: null,
       );
   @override
-  _i13.Future<_i9.Example?> getDefaultExampleBySdk(_i12.Sdk? sdk) =>
+  _i14.Future<_i9.Example?> getDefaultExampleBySdk(_i12.Sdk? sdk) =>
       (super.noSuchMethod(
         Invocation.method(
           #getDefaultExampleBySdk,
           [sdk],
         ),
         returnValue: Future<_i9.Example?>.value(),
-      ) as _i13.Future<_i9.Example?>);
+      ) as _i14.Future<_i9.Example?>);
   @override
-  _i13.Future<void> loadDefaultPrecompiledObjects() => (super.noSuchMethod(
+  _i14.Future<void> loadDefaultPrecompiledObjects() => (super.noSuchMethod(
         Invocation.method(
           #loadDefaultPrecompiledObjects,
           [],
         ),
         returnValue: Future<void>.value(),
         returnValueForMissingStub: Future<void>.value(),
-      ) as _i13.Future<void>);
+      ) as _i14.Future<void>);
   @override
-  _i13.Future<void> loadDefaultPrecompiledObjectsIfNot() => 
(super.noSuchMethod(
+  _i14.Future<void> loadDefaultPrecompiledObjectsIfNot() => 
(super.noSuchMethod(
         Invocation.method(
           #loadDefaultPrecompiledObjectsIfNot,
           [],
         ),
         returnValue: Future<void>.value(),
         returnValueForMissingStub: Future<void>.value(),
-      ) as _i13.Future<void>);
+      ) as _i14.Future<void>);
   @override
-  _i13.Future<_i8.ExampleBase?> getCatalogExampleByPath(String? path) =>
+  _i14.Future<_i8.ExampleBase?> getCatalogExampleByPath(String? path) =>
       (super.noSuchMethod(
         Invocation.method(
           #getCatalogExampleByPath,
           [path],
         ),
         returnValue: Future<_i8.ExampleBase?>.value(),
-      ) as _i13.Future<_i8.ExampleBase?>);
+      ) as _i14.Future<_i8.ExampleBase?>);
   @override
   void addListener(_i15.VoidCallback? listener) => super.noSuchMethod(
         Invocation.method(
diff --git 
a/playground/frontend/playground_components/test/src/controllers/example_loaders/http_example_loader_test.dart
 
b/playground/frontend/playground_components/test/src/controllers/example_loaders/http_example_loader_test.dart
index 5860ff7bb17..b44ec6f81b5 100644
--- 
a/playground/frontend/playground_components/test/src/controllers/example_loaders/http_example_loader_test.dart
+++ 
b/playground/frontend/playground_components/test/src/controllers/example_loaders/http_example_loader_test.dart
@@ -51,7 +51,7 @@ void main() {
     // TODO(alexeyinkin): Compare whole objects when that gets to include all 
fields, https://github.com/apache/beam/issues/23979
     expect(example.name, _name);
     expect(example.sdk, _sdk);
-    expect(example.source, _contents);
+    expect(example.files.first.content, _contents);
     expect(example.type, ExampleType.example);
     expect(example.path, _path);
   });
diff --git 
a/playground/frontend/playground_components/test/src/controllers/example_loaders/http_example_loader_test.mocks.dart
 
b/playground/frontend/playground_components/test/src/controllers/example_loaders/http_example_loader_test.mocks.dart
index ad8a7a54490..a697f1105ad 100644
--- 
a/playground/frontend/playground_components/test/src/controllers/example_loaders/http_example_loader_test.mocks.dart
+++ 
b/playground/frontend/playground_components/test/src/controllers/example_loaders/http_example_loader_test.mocks.dart
@@ -14,8 +14,7 @@ import 
'package:playground_components/src/models/example.dart' as _i3;
 import 'package:playground_components/src/models/example_base.dart' as _i2;
 import 'package:playground_components/src/models/loading_status.dart' as _i8;
 import 'package:playground_components/src/models/sdk.dart' as _i5;
-import 'package:playground_components/src/repositories/models/shared_file.dart'
-    as _i9;
+import 'package:playground_components/src/models/snippet_file.dart' as _i9;
 
 // ignore_for_file: type=lint
 // ignore_for_file: avoid_redundant_argument_values
@@ -121,7 +120,7 @@ class MockExampleCache extends _i1.Mock implements 
_i4.ExampleCache {
       ) as _i7.Future<_i3.Example>);
   @override
   _i7.Future<String> saveSnippet({
-    List<_i9.SharedFile>? files,
+    List<_i9.SnippetFile>? files,
     _i5.Sdk? sdk,
     String? pipelineOptions,
   }) =>
diff --git 
a/playground/frontend/playground_components/test/src/controllers/playground_controller_test.dart
 
b/playground/frontend/playground_components/test/src/controllers/playground_controller_test.dart
index 0adfff718aa..30d39e293c9 100644
--- 
a/playground/frontend/playground_components/test/src/controllers/playground_controller_test.dart
+++ 
b/playground/frontend/playground_components/test/src/controllers/playground_controller_test.dart
@@ -63,10 +63,12 @@ Future<void> main() async {
       expect(controller.pipelineOptions, '');
     });
 
-    test('Initial value of source should be empty string', () {
+    test('source', () {
       expect(controller.source, null);
       controller.setSdk(Sdk.go);
-      expect(controller.source, '');
+      expect(controller.source, null);
+      controller.snippetEditingController!.setExample(exampleGo4Multifile);
+      expect(controller.source, exampleGo4Multifile.files[1].content);
     });
 
     group('isExampleChanged Tests', () {
@@ -74,12 +76,13 @@ Future<void> main() async {
         'If example source is changed, value of isExampleChanged should be 
true',
         () {
           controller.setExample(
-            exampleMock1,
+            examplePython1,
             descriptor: emptyDescriptor,
             setCurrentSdk: true,
           );
           expect(controller.isExampleChanged, false);
-          controller.setSource('test');
+          controller.snippetEditingController?.fileControllers.first
+              .codeController.text = 'test';
           expect(controller.isExampleChanged, true);
         },
       );
@@ -88,7 +91,7 @@ Future<void> main() async {
         'If pipelineOptions is changed, value of isExampleChanged should be 
true',
         () {
           controller.setExample(
-            exampleMock1,
+            examplePython1,
             descriptor: emptyDescriptor,
             setCurrentSdk: true,
           );
@@ -103,7 +106,7 @@ Future<void> main() async {
       'If selected example type is not test and SDK is java or python, graph 
should be available',
       () {
         controller.setExample(
-          exampleMock1,
+          examplePython1,
           descriptor: emptyDescriptor,
           setCurrentSdk: true,
         );
@@ -116,11 +119,11 @@ Future<void> main() async {
       () {
         controller.addListener(() {
           expect(controller.sdk, Sdk.go);
-          expect(controller.source, exampleMockGo.source);
-          expect(controller.selectedExample, exampleMockGo);
+          expect(controller.source, exampleGo6.files.first.content);
+          expect(controller.selectedExample, exampleGo6);
         });
         controller.setExample(
-          exampleMockGo,
+          exampleGo6,
           descriptor: emptyDescriptor,
           setCurrentSdk: true,
         );
@@ -138,13 +141,14 @@ Future<void> main() async {
         'Playground state reset should reset source to example notify all 
listeners',
         () {
       controller.setExample(
-        exampleMock1,
+        examplePython1,
         descriptor: emptyDescriptor,
         setCurrentSdk: true,
       );
-      controller.setSource('source');
+      controller.snippetEditingController?.fileControllers.first.codeController
+          .text = 'source';
       controller.addListener(() {
-        expect(controller.source, exampleMock1.source);
+        expect(controller.source, examplePython1.files.first.content);
       });
       controller.reset();
     });
@@ -170,12 +174,12 @@ Future<void> main() async {
 
     test('getLoadingDescriptor()', () {
       controller.setExample(
-        exampleMock2,
+        examplePython2,
         descriptor: standardDescriptor2,
         setCurrentSdk: true,
       );
       controller.setExample(
-        exampleMockGo,
+        exampleGo6,
         descriptor: standardGoDescriptor,
         setCurrentSdk: false,
       );
@@ -199,7 +203,8 @@ Future<void> main() async {
 
       expect(controller.sdk, Sdk.go);
       expect(
-        controller.requireSnippetEditingController().codeController.text,
+        controller.snippetEditingController?.fileControllers.first
+            .codeController.fullText,
         '',
       );
 
@@ -221,11 +226,13 @@ Future<void> main() async {
 
         expect(controller.sdk, Sdk.go);
 
-        controller.requireSnippetEditingController().setSource(text);
+        controller.snippetEditingController?.fileControllers.first
+            .codeController.text = text;
         controller.setEmptyIfNotExists(Sdk.go, setCurrentSdk: true);
 
         expect(
-          controller.requireSnippetEditingController().codeController.text,
+          controller.snippetEditingController?.fileControllers.first
+              .codeController.fullText,
           text,
         );
       });
diff --git 
a/playground/frontend/playground_components/test/src/controllers/playground_controller_test.mocks.dart
 
b/playground/frontend/playground_components/test/src/controllers/playground_controller_test.mocks.dart
index e44e628a040..9bccb3f91f7 100644
--- 
a/playground/frontend/playground_components/test/src/controllers/playground_controller_test.mocks.dart
+++ 
b/playground/frontend/playground_components/test/src/controllers/playground_controller_test.mocks.dart
@@ -22,8 +22,7 @@ import 
'package:playground_components/src/models/example_loading_descriptors/exa
     as _i8;
 import 'package:playground_components/src/models/loading_status.dart' as _i12;
 import 'package:playground_components/src/models/sdk.dart' as _i9;
-import 'package:playground_components/src/repositories/models/shared_file.dart'
-    as _i13;
+import 'package:playground_components/src/models/snippet_file.dart' as _i13;
 
 // ignore_for_file: type=lint
 // ignore_for_file: avoid_redundant_argument_values
@@ -175,7 +174,7 @@ class MockExampleCache extends _i1.Mock implements 
_i10.ExampleCache {
       ) as _i7.Future<_i4.Example>);
   @override
   _i7.Future<String> saveSnippet({
-    List<_i13.SharedFile>? files,
+    List<_i13.SnippetFile>? files,
     _i9.Sdk? sdk,
     String? pipelineOptions,
   }) =>
diff --git 
a/playground/frontend/playground_components/test/src/controllers/snippet_editing_controller_test.dart
 
b/playground/frontend/playground_components/test/src/controllers/snippet_editing_controller_test.dart
index 8ad2a575a1f..376a27469ac 100644
--- 
a/playground/frontend/playground_components/test/src/controllers/snippet_editing_controller_test.dart
+++ 
b/playground/frontend/playground_components/test/src/controllers/snippet_editing_controller_test.dart
@@ -23,6 +23,7 @@ import 
'package:playground_components/src/controllers/snippet_editing_controller
 import 'package:playground_components/src/enums/complexity.dart';
 import 
'package:playground_components/src/models/example_loading_descriptors/content_example_loading_descriptor.dart';
 import 'package:playground_components/src/models/sdk.dart';
+import 'package:playground_components/src/models/snippet_file.dart';
 import 'package:playground_components/src/playground_components.dart';
 
 import '../common/descriptors.dart';
@@ -34,52 +35,54 @@ void main() async {
   int notified = 0;
   late SnippetEditingController controller;
 
-  setUp((){
+  setUp(() {
     notified = 0;
     controller = SnippetEditingController(sdk: Sdk.python);
     controller.addListener(() => notified++);
   });
 
   group('SnippetEditingController.', () {
-    group('Changes.', (){
-      test('Unchanged initially', (){
+    group('Changes.', () {
+      test('Unchanged initially', () {
         expect(controller.isChanged, false);
         expect(notified, 0);
       });
 
       test('Unchanged after setting an example', () {
-        controller.setExample(exampleMock1);
+        controller.setExample(examplePython1);
 
         expect(controller.isChanged, false);
         expect(notified, 1);
       });
 
       test('Changes when changing code, notifies once', () {
-        controller.setExample(exampleMock1);
-        controller.codeController.text = exampleMock1.source;
+        controller.setExample(examplePython1);
+        controller.fileControllers.first.codeController.text =
+            examplePython1.files.first.content;
 
         expect(controller.isChanged, false);
         expect(notified, 1);
 
-        controller.codeController.text = 'changed';
+        controller.fileControllers.first.codeController.text = 'changed';
 
         expect(controller.isChanged, true);
         expect(notified, 2);
 
-        controller.codeController.text = 'changed2';
+        controller.fileControllers.first.codeController.text = 'changed2';
 
         expect(controller.isChanged, true);
         expect(notified, 2);
 
-        controller.codeController.text = exampleMock1.source;
+        controller.fileControllers.first.codeController.text =
+            examplePython1.files.first.content;
 
         expect(controller.isChanged, false);
         expect(notified, 3);
       });
 
       test('Changes when changing pipelineOptions, notifies once', () {
-        controller.setExample(exampleGoPipelineOptions);
-        controller.pipelineOptions = exampleGoPipelineOptions.pipelineOptions;
+        controller.setExample(exampleGo5PipelineOptions);
+        controller.pipelineOptions = exampleGo5PipelineOptions.pipelineOptions;
 
         expect(controller.isChanged, false);
         expect(notified, 1);
@@ -94,17 +97,64 @@ void main() async {
         expect(controller.isChanged, true);
         expect(notified, 2);
 
-        controller.pipelineOptions = exampleGoPipelineOptions.pipelineOptions;
+        controller.pipelineOptions = exampleGo5PipelineOptions.pipelineOptions;
 
         expect(controller.isChanged, false);
         expect(notified, 3);
       });
     });
 
+    group('Files.', () {
+      test('activeFileController, activateFileControllerByName', () {
+        expect(controller.activeFileController, null);
+
+        controller.setExample(exampleGo4Multifile);
+
+        expect(
+          controller.activeFileController?.getFile().content,
+          exampleGo4Multifile.files[1].content,
+        );
+
+        controller.activateFileControllerByName(
+          exampleGo4Multifile.files[0].name,
+        );
+        expect(
+          controller.activeFileController?.getFile().content,
+          exampleGo4Multifile.files[0].content,
+        );
+
+        controller.activateFileControllerByName('nonexistent');
+        expect(controller.activeFileController, null);
+      });
+
+      test('getFileControllerByName', () {
+        controller.setExample(exampleGo4Multifile);
+
+        expect(
+          controller
+              .getFileControllerByName(exampleGo4Multifile.files[0].name)
+              ?.savedFile
+              .content,
+          exampleGo4Multifile.files[0].content,
+        );
+        expect(
+          controller
+              .getFileControllerByName(exampleGo4Multifile.files[1].name)
+              ?.savedFile
+              .content,
+          exampleGo4Multifile.files[1].content,
+        );
+        expect(
+          controller.getFileControllerByName('nonexistent'),
+          null,
+        );
+      });
+    });
+
     group('Descriptors.', () {
       test('Returns the original descriptor if unchanged', () {
         controller.setExample(
-          exampleMock1,
+          examplePython1,
           descriptor: standardDescriptor1,
         );
 
@@ -115,32 +165,33 @@ void main() async {
 
       test('Returns a ContentExampleLoadingDescriptor if changed', () {
         controller.setExample(
-          exampleMock1,
+          examplePython1,
           descriptor: standardDescriptor1,
         );
 
-        controller.codeController.value = const TextEditingValue(text: 'ex4');
+        controller.fileControllers.first.codeController.value =
+            const TextEditingValue(text: 'ex4');
         final descriptor = controller.getLoadingDescriptor();
 
         const expected = ContentExampleLoadingDescriptor(
-          content: 'ex4',
-          sdk: Sdk.python,
-          name: 'Example X1',
           complexity: Complexity.basic,
+          files: [SnippetFile(content: 'ex4', isMain: true, name: '')],
+          name: 'Example X1',
+          sdk: Sdk.python,
         );
 
         expect(descriptor, expected);
       });
 
       test('Returns a ContentExampleLoadingDescriptor if no descriptor', () {
-        controller.setExample(exampleMock1, descriptor: null);
+        controller.setExample(examplePython1, descriptor: null);
 
-        controller.setExample(exampleMock2, descriptor: null);
+        controller.setExample(examplePython2, descriptor: null);
         final descriptor = controller.getLoadingDescriptor();
 
         const expected = ContentExampleLoadingDescriptor(
           complexity: Complexity.basic,
-          content: 'ex2',
+          files: [SnippetFile(content: 'ex2', isMain: true, name: '')],
           name: 'Kata',
           sdk: Sdk.python,
         );
diff --git 
a/playground/frontend/playground_components/test/src/models/example_loading_descriptors/content_example_loading_descriptor_test.dart
 
b/playground/frontend/playground_components/test/src/models/example_loading_descriptors/content_example_loading_descriptor_test.dart
index 75a6aa451a0..d8e5a70c3ea 100644
--- 
a/playground/frontend/playground_components/test/src/models/example_loading_descriptors/content_example_loading_descriptor_test.dart
+++ 
b/playground/frontend/playground_components/test/src/models/example_loading_descriptors/content_example_loading_descriptor_test.dart
@@ -25,20 +25,19 @@ void main() {
   group('ContentExampleLoadingDescriptor', () {
     test('defaults', () {
       const descriptorWithDefaults = ContentExampleLoadingDescriptor(
-        content: 'abc',
+        files: [SnippetFile(content: 'abc', isMain: true)],
         sdk: Sdk.go,
       );
 
-      final parsed = ContentExampleLoadingDescriptor.tryParse(
-        descriptorWithDefaults.toJson(),
-      );
+      final map = descriptorWithDefaults.toJson();
+      final parsed = ContentExampleLoadingDescriptor.tryParse(map);
 
       expect(parsed, descriptorWithDefaults);
     });
 
     const descriptor = ContentExampleLoadingDescriptor(
       complexity: Complexity.advanced,
-      content: 'abc',
+      files: [SnippetFile(content: 'abc', isMain: true)],
       name: 'name',
       sdk: Sdk.go,
     );
diff --git 
a/playground/frontend/playground_components/test/src/repositories/code_repository_test.dart
 
b/playground/frontend/playground_components/test/src/repositories/code_repository_test.dart
index 7615fb115d7..6dfd362282f 100644
--- 
a/playground/frontend/playground_components/test/src/repositories/code_repository_test.dart
+++ 
b/playground/frontend/playground_components/test/src/repositories/code_repository_test.dart
@@ -20,6 +20,7 @@ import 'package:flutter_test/flutter_test.dart';
 import 'package:mockito/annotations.dart';
 import 'package:mockito/mockito.dart';
 import 'package:playground_components/src/models/sdk.dart';
+import 'package:playground_components/src/models/snippet_file.dart';
 import 
'package:playground_components/src/repositories/code_client/code_client.dart';
 import 'package:playground_components/src/repositories/code_repository.dart';
 import 
'package:playground_components/src/repositories/models/check_status_response.dart';
@@ -31,7 +32,7 @@ import 
'package:playground_components/src/repositories/models/run_code_result.da
 import 'code_repository_test.mocks.dart';
 
 const kRequestMock = RunCodeRequest(
-  code: 'code',
+  files: [SnippetFile(content: 'code', isMain: true)],
   sdk: Sdk.java,
   pipelineOptions: {},
   datasets: [],
diff --git 
a/playground/frontend/playground_components/test/src/repositories/example_repository_test.dart
 
b/playground/frontend/playground_components/test/src/repositories/example_repository_test.dart
index 964f3b34b59..6b7f740d4a4 100644
--- 
a/playground/frontend/playground_components/test/src/repositories/example_repository_test.dart
+++ 
b/playground/frontend/playground_components/test/src/repositories/example_repository_test.dart
@@ -46,31 +46,36 @@ void main() {
         await repo.getPrecompiledObjects(kGetPrecompiledObjectsRequest),
         kGetPrecompiledObjectsResponse.categories,
       );
-      
verify(client.getPrecompiledObjects(kGetPrecompiledObjectsRequest)).called(1);
+      verify(client.getPrecompiledObjects(kGetPrecompiledObjectsRequest))
+          .called(1);
     },
   );
 
   test(
     'Example repository getDefaultExample should return defaultExample for 
chosen Sdk',
     () async {
-      
when(client.getDefaultPrecompiledObject(kGetDefaultPrecompiledObjectRequest))
+      when(client
+              
.getDefaultPrecompiledObject(kGetDefaultPrecompiledObjectRequest))
           .thenAnswer((_) async => kGetDefaultPrecompiledObjectResponse);
       expect(
-        await 
repo.getDefaultPrecompiledObject(kGetDefaultPrecompiledObjectRequest),
+        await repo
+            .getDefaultPrecompiledObject(kGetDefaultPrecompiledObjectRequest),
         kGetDefaultPrecompiledObjectResponse.example,
       );
-      
verify(client.getDefaultPrecompiledObject(kGetDefaultPrecompiledObjectRequest)).called(1);
+      verify(client
+              
.getDefaultPrecompiledObject(kGetDefaultPrecompiledObjectRequest))
+          .called(1);
     },
   );
 
   test(
-    'Example repository getExampleSource should return source code for 
example',
+    'Example repository getExampleSource should return files for example',
     () async {
       when(client.getPrecompiledObjectCode(kRequestForExampleInfo))
           .thenAnswer((_) async => kGetPrecompiledObjectCodeResponse);
       expect(
         await repo.getPrecompiledObjectCode(kRequestForExampleInfo),
-        kGetPrecompiledObjectCodeResponse.code,
+        kGetPrecompiledObjectCodeResponse.files,
       );
       
verify(client.getPrecompiledObjectCode(kRequestForExampleInfo)).called(1);
     },
@@ -85,7 +90,8 @@ void main() {
         await repo.getPrecompiledObjectOutput(kRequestForExampleInfo),
         kOutputResponse.output,
       );
-      
verify(client.getPrecompiledObjectOutput(kRequestForExampleInfo)).called(1);
+      verify(client.getPrecompiledObjectOutput(kRequestForExampleInfo))
+          .called(1);
     },
   );
 
@@ -111,13 +117,14 @@ void main() {
         await repo.getPrecompiledObjectGraph(kRequestForExampleInfo),
         kOutputResponse.output,
       );
-      
verify(client.getPrecompiledObjectGraph(kRequestForExampleInfo)).called(1);
+      verify(client.getPrecompiledObjectGraph(kRequestForExampleInfo))
+          .called(1);
     },
   );
 
   test(
     'Example repository getExample should return ExampleModel',
-        () async {
+    () async {
       when(client.getPrecompiledObject(kRequestForExampleInfo))
           .thenAnswer((_) async => kGetDefaultPrecompiledObjectResponse);
       expect(
diff --git a/playground/frontend/playground_components_dev/pubspec.yaml 
b/playground/frontend/playground_components_dev/pubspec.yaml
index a4998c7c1bf..0630dce0633 100644
--- a/playground/frontend/playground_components_dev/pubspec.yaml
+++ b/playground/frontend/playground_components_dev/pubspec.yaml
@@ -26,7 +26,7 @@ environment:
 
 dependencies:
   flutter: { sdk: flutter }
-  flutter_code_editor: ^0.2.4
+  flutter_code_editor: ^0.2.5
   flutter_test: { sdk: flutter }
   highlight: ^0.7.0
   http: ^0.13.5
diff --git a/playground/frontend/pubspec.lock b/playground/frontend/pubspec.lock
index db8ae40f111..c5d387dc1ec 100644
--- a/playground/frontend/pubspec.lock
+++ b/playground/frontend/pubspec.lock
@@ -292,7 +292,7 @@ packages:
       name: flutter_code_editor
       url: "https://pub.dartlang.org";
     source: hosted
-    version: "0.2.4"
+    version: "0.2.5"
   flutter_driver:
     dependency: transitive
     description: flutter
@@ -491,6 +491,13 @@ packages:
       url: "https://pub.dartlang.org";
     source: hosted
     version: "4.7.0"
+  keyed_collection_widgets:
+    dependency: transitive
+    description:
+      name: keyed_collection_widgets
+      url: "https://pub.dartlang.org";
+    source: hosted
+    version: "0.4.3"
   linked_scroll_controller:
     dependency: transitive
     description:
diff --git a/playground/frontend/pubspec.yaml b/playground/frontend/pubspec.yaml
index 10b92e26371..3c65a5aab12 100644
--- a/playground/frontend/pubspec.yaml
+++ b/playground/frontend/pubspec.yaml
@@ -53,7 +53,7 @@ dependencies:
 dev_dependencies:
   build_runner: ^2.1.4
   fake_async: ^1.3.0
-  flutter_code_editor: ^0.2.4
+  flutter_code_editor: ^0.2.5
   flutter_lints: ^2.0.1
   flutter_test: { sdk: flutter }
   integration_test: { sdk: flutter }
diff --git 
a/playground/frontend/test/modules/messages/models/set_content_message_test.dart
 
b/playground/frontend/test/modules/messages/models/set_content_message_test.dart
index 02ac67dcd44..45169f4d224 100644
--- 
a/playground/frontend/test/modules/messages/models/set_content_message_test.dart
+++ 
b/playground/frontend/test/modules/messages/models/set_content_message_test.dart
@@ -21,7 +21,8 @@ import 
'package:playground/modules/examples/models/example_loading_descriptors/e
 import 'package:playground/modules/messages/models/set_content_message.dart';
 import 'package:playground_components/playground_components.dart';
 
-const _content = 'my_code';
+const _content1 = 'my_code1';
+const _content2 = 'my_code2';
 const _sdk = Sdk.python;
 
 void main() {
@@ -94,19 +95,26 @@ void main() {
               [],
               {'type': 'any-other'},
               {
-                'content': _content,
+                'files': [
+                  {'content': _content1, 'isMain': false, 'name': '1'},
+                  {'content': _content2, 'isMain': true, 'name': '2'},
+                ],
                 'name': 'name',
                 'sdk': _sdk.id,
                 'complexity': 'basic',
               },
               {
-                'content': _content,
+                'files': [
+                  {'content': _content1, 'isMain': false, 'name': '1'}
+                ],
                 'name': null,
                 'sdk': _sdk.id,
                 'complexity': 'medium',
               },
               {
-                'content': _content,
+                'files': [
+                  {'content': _content1, 'isMain': false, 'name': '1'}
+                ],
                 'sdk': _sdk.id,
                 'complexity': 'advanced',
               },
@@ -122,22 +130,29 @@ void main() {
             descriptor: ExamplesLoadingDescriptor(
               descriptors: const [
                 ContentExampleLoadingDescriptor(
-                  content: _content,
+                  complexity: Complexity.basic,
+                  files: [
+                    SnippetFile(content: _content1, isMain: false, name: '1'),
+                    SnippetFile(content: _content2, isMain: true, name: '2'),
+                  ],
                   name: 'name',
                   sdk: _sdk,
-                  complexity: Complexity.basic,
                 ),
                 ContentExampleLoadingDescriptor(
-                  content: _content,
+                  complexity: Complexity.medium,
+                  files: [
+                    SnippetFile(content: _content1, isMain: false, name: '1'),
+                  ],
                   name: null,
                   sdk: _sdk,
-                  complexity: Complexity.medium,
                 ),
                 ContentExampleLoadingDescriptor(
-                  content: _content,
+                  complexity: Complexity.advanced,
+                  files: [
+                    SnippetFile(content: _content1, isMain: false, name: '1'),
+                  ],
                   name: null,
                   sdk: _sdk,
-                  complexity: Complexity.advanced,
                 ),
               ],
               lazyLoadDescriptors:


Reply via email to