diff --git a/packages/devtools_app/lib/devtools_app.dart b/packages/devtools_app/lib/devtools_app.dart index ddfa7466ab2..5917216e754 100644 --- a/packages/devtools_app/lib/devtools_app.dart +++ b/packages/devtools_app/lib/devtools_app.dart @@ -29,6 +29,7 @@ export 'src/screens/inspector/inspector_screen_body.dart'; export 'src/screens/inspector/inspector_tree_controller.dart'; export 'src/screens/inspector_shared/inspector_screen.dart'; export 'src/screens/inspector_shared/inspector_screen_controller.dart'; +export 'src/screens/logging/log_details_controller.dart'; export 'src/screens/logging/logging_controller.dart'; export 'src/screens/logging/logging_screen.dart'; export 'src/screens/memory/framework/memory_controller.dart'; diff --git a/packages/devtools_app/lib/src/screens/debugger/codeview.dart b/packages/devtools_app/lib/src/screens/debugger/codeview.dart index 19ca7e44b5c..d5f1ebd54da 100644 --- a/packages/devtools_app/lib/src/screens/debugger/codeview.dart +++ b/packages/devtools_app/lib/src/screens/debugger/codeview.dart @@ -26,6 +26,7 @@ import '../../shared/ui/common_widgets.dart'; import '../../shared/ui/history_viewport.dart'; import '../../shared/ui/hover.dart'; import '../../shared/ui/search.dart'; +import '../../shared/ui/search_highlighter.dart'; import '../../shared/ui/utils.dart'; import '../vm_developer/vm_service_private_extensions.dart'; import 'breakpoints.dart'; @@ -1272,131 +1273,22 @@ class _HoverableLine extends StatelessWidget { return null; } - List _contentsWithMatch( - List startingContents, - SourceToken match, - Color matchColor, { - required BuildContext context, - }) { - final contentsWithMatch = []; - var startColumnForSpan = 0; - for (final span in startingContents) { - final spanText = span.toPlainText(); - final startColumnForMatch = match.position.column!; - if (startColumnForSpan <= startColumnForMatch && - startColumnForSpan + spanText.length > startColumnForMatch) { - // The active search is part of this [span]. - final matchStartInSpan = startColumnForMatch - startColumnForSpan; - final matchEndInSpan = matchStartInSpan + match.length; - - // Add the part of [span] that occurs before the search match. - contentsWithMatch.add( - TextSpan( - text: spanText.substring(0, matchStartInSpan), - style: span.style, - ), - ); - - final matchStyle = (span.style ?? DefaultTextStyle.of(context).style) - .copyWith(color: Colors.black, backgroundColor: matchColor); - - if (matchEndInSpan <= spanText.length) { - final matchText = spanText.substring( - matchStartInSpan, - matchEndInSpan, - ); - final trailingText = spanText.substring(matchEndInSpan); - // Add the match and any part of [span] that occurs after the search - // match. - contentsWithMatch.addAll([ - TextSpan(text: matchText, style: matchStyle), - if (trailingText.isNotEmpty) - TextSpan( - text: spanText.substring(matchEndInSpan), - style: span.style, - ), - ]); - } else { - // In this case, the active search match exists across multiple spans, - // so we need to add the part of the match that is in this [span] and - // continue looking for the remaining part of the match in the spans - // to follow. - contentsWithMatch.add( - TextSpan( - text: spanText.substring(matchStartInSpan), - style: matchStyle, - ), - ); - final remainingMatchLength = - match.length - (spanText.length - matchStartInSpan); - match = SourceToken( - position: SourcePosition( - line: match.position.line, - column: startColumnForMatch + match.length - remainingMatchLength, - ), - length: remainingMatchLength, - ); - } - } else { - contentsWithMatch.add(span); - } - startColumnForSpan += spanText.length; - } - return contentsWithMatch; - } - TextSpan searchAwareLineContents(BuildContext context) { // If syntax highlighting is disabled for the script, then // `lineContents` is simply a `TextSpan` with no children. final lineContentsSpans = lineContents.children ?? [lineContents]; - final activeSearchAwareContents = _activeSearchAwareLineContents( - lineContentsSpans, - context: context, - ); - final allSearchAwareContents = _searchMatchAwareLineContents( - activeSearchAwareContents!, - context: context, - ); + final theme = Theme.of(context); + return TextSpan( - children: allSearchAwareContents, + children: SearchHighlighter.highlightSpans( + lineContentsSpans.cast(), + matches: searchMatches?.map((m) => m.range).toList() ?? [], + activeMatch: activeSearchMatch?.range, + style: theme.regularTextStyle, + ), style: lineContents.style, ); } - - List? _activeSearchAwareLineContents( - List startingContents, { - required BuildContext context, - }) { - final match = activeSearchMatch; - if (match == null) return startingContents; - return _contentsWithMatch( - startingContents, - match, - activeSearchMatchColor, - context: context, - ); - } - - List _searchMatchAwareLineContents( - List startingContents, { - required BuildContext context, - }) { - final matches = searchMatches; - if (matches == null || matches.isEmpty) return startingContents; - final searchMatchesToFind = List.of(matches) - ..remove(activeSearchMatch); - - var contentsWithMatch = startingContents; - for (final match in searchMatchesToFind) { - contentsWithMatch = _contentsWithMatch( - contentsWithMatch, - match, - searchMatchColor, - context: context, - ); - } - return contentsWithMatch; - } } class ScriptPopupMenu extends StatelessWidget { diff --git a/packages/devtools_app/lib/src/screens/debugger/debugger_model.dart b/packages/devtools_app/lib/src/screens/debugger/debugger_model.dart index 12affb77fdf..f69e302bc01 100644 --- a/packages/devtools_app/lib/src/screens/debugger/debugger_model.dart +++ b/packages/devtools_app/lib/src/screens/debugger/debugger_model.dart @@ -52,6 +52,8 @@ class SourceToken with SearchableDataMixin { final int length; + Range get range => Range(position.column!, position.column! + length); + @override String toString() { return '$position-${position.column! + length}'; diff --git a/packages/devtools_app/lib/src/screens/logging/_log_details.dart b/packages/devtools_app/lib/src/screens/logging/_log_details.dart index 977af418050..f277c54873c 100644 --- a/packages/devtools_app/lib/src/screens/logging/_log_details.dart +++ b/packages/devtools_app/lib/src/screens/logging/_log_details.dart @@ -11,12 +11,16 @@ import 'package:flutter/material.dart'; import '../../shared/globals.dart'; import '../../shared/preferences/preferences.dart'; import '../../shared/ui/common_widgets.dart'; +import '../../shared/ui/search.dart'; +import '../../shared/ui/search_highlighter.dart'; +import 'log_details_controller.dart'; import 'logging_controller.dart'; class LogDetails extends StatefulWidget { - const LogDetails({super.key, required this.log}); + const LogDetails({super.key, required this.log, required this.controller}); final LogData? log; + final LogDetailsController controller; @override State createState() => _LogDetailsState(); @@ -45,6 +49,10 @@ class _LogDetailsState extends State if (widget.log != oldWidget.log) { unawaited(_computeLogDetails()); } + if (widget.controller != oldWidget.controller) { + cancelListeners(); + addAutoDisposeListener(preferences.logging.detailsFormat); + } } Future _computeLogDetails() async { @@ -81,6 +89,7 @@ class _LogDetailsState extends State header: _LogDetailsHeader( log: log, format: preferences.logging.detailsFormat.value, + controller: widget.controller, ), child: Scrollbar( controller: scrollController, @@ -93,9 +102,9 @@ class _LogDetailsState extends State ? Padding( padding: const EdgeInsets.all(denseSpacing), child: SelectionArea( - child: Text( - log?.prettyPrinted() ?? '', - textAlign: TextAlign.left, + child: _SearchableLogDetailsText( + text: log?.prettyPrinted() ?? '', + controller: widget.controller, ), ), ) @@ -107,10 +116,15 @@ class _LogDetailsState extends State } class _LogDetailsHeader extends StatelessWidget { - const _LogDetailsHeader({required this.log, required this.format}); + const _LogDetailsHeader({ + required this.log, + required this.format, + required this.controller, + }); final LogData? log; final LoggingDetailsFormat format; + final LogDetailsController controller; @override Widget build(BuildContext context) { @@ -122,7 +136,13 @@ class _LogDetailsHeader extends StatelessWidget { title: const Text('Details'), includeTopBorder: false, roundedTopBorder: false, + tall: true, actions: [ + // Only supporting search for the text format now since supporting this + // for the expandable JSON viewer would require a more complicated + // refactor of that shared component. + if (format == LoggingDetailsFormat.text) + _LogDetailsSearchField(controller: controller, log: log), LogDetailsFormatButton(format: format), const SizedBox(width: densePadding), CopyToClipboardControl( @@ -134,6 +154,108 @@ class _LogDetailsHeader extends StatelessWidget { } } +/// An animated search field for the log details view that toggles between an icon +/// and a full [SearchField]. +class _LogDetailsSearchField extends StatefulWidget { + const _LogDetailsSearchField({required this.controller, required this.log}); + + final LogDetailsController controller; + final LogData? log; + + @override + State<_LogDetailsSearchField> createState() => _LogDetailsSearchFieldState(); +} + +class _LogDetailsSearchFieldState extends State<_LogDetailsSearchField> + with AutoDisposeMixin { + late bool _isExpanded; + + @override + void initState() { + super.initState(); + _isExpanded = widget.controller.search.isNotEmpty; + addAutoDisposeListener(widget.controller.searchFieldFocusNode, () { + final hasFocus = + widget.controller.searchFieldFocusNode?.hasFocus ?? false; + if (hasFocus != _isExpanded) { + setState(() { + _isExpanded = hasFocus; + }); + } + }); + } + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: defaultDuration, + curve: defaultCurve, + width: _isExpanded ? mediumSearchFieldWidth : defaultButtonHeight, + child: OverflowBox( + minWidth: 0.0, + maxWidth: mediumSearchFieldWidth, + child: _isExpanded + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: densePadding), + child: SearchField( + searchController: widget.controller, + searchFieldEnabled: + widget.log != null && widget.log!.details != null, + shouldRequestFocus: true, + searchFieldWidth: mediumSearchFieldWidth, + ), + ) + : ToolbarAction( + icon: Icons.search, + tooltip: 'Search details', + size: defaultIconSize, + onPressed: () { + setState(() { + _isExpanded = true; + }); + widget.controller.searchFieldFocusNode?.requestFocus(); + }, + ), + ), + ); + } +} + +/// A text widget for the log details view that highlights search matches. +class _SearchableLogDetailsText extends StatelessWidget { + const _SearchableLogDetailsText({ + required this.text, + required this.controller, + }); + + final String text; + final LogDetailsController controller; + + @override + Widget build(BuildContext context) { + return MultiValueListenableBuilder( + listenables: [controller.searchMatches, controller.activeSearchMatch], + builder: (context, values, _) { + final theme = Theme.of(context); + + final matches = (values[0] as List) + .map((m) => m.range) + .toList(); + final activeMatch = (values[1] as LogDetailsMatch?)?.range; + + return Text.rich( + SearchHighlighter.highlight( + text, + matches, + activeMatch: activeMatch, + style: theme.regularTextStyle, + ), + ); + }, + ); + } +} + @visibleForTesting class LogDetailsFormatButton extends StatelessWidget { const LogDetailsFormatButton({super.key, required this.format}); diff --git a/packages/devtools_app/lib/src/screens/logging/_logs_table.dart b/packages/devtools_app/lib/src/screens/logging/_logs_table.dart index 0475919955a..c406a428d45 100644 --- a/packages/devtools_app/lib/src/screens/logging/_logs_table.dart +++ b/packages/devtools_app/lib/src/screens/logging/_logs_table.dart @@ -48,6 +48,7 @@ class LogsTable extends StatelessWidget { defaultSortDirection: SortDirection.ascending, secondarySortColumn: messageColumn, rowHeight: _logRowHeight, + tallHeaders: true, ); } } diff --git a/packages/devtools_app/lib/src/screens/logging/log_details_controller.dart b/packages/devtools_app/lib/src/screens/logging/log_details_controller.dart new file mode 100644 index 00000000000..33eec934c63 --- /dev/null +++ b/packages/devtools_app/lib/src/screens/logging/log_details_controller.dart @@ -0,0 +1,62 @@ +// Copyright 2024 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +import 'package:devtools_app_shared/utils.dart'; +import 'package:flutter/foundation.dart'; + +import '../../shared/primitives/utils.dart'; +import '../../shared/ui/search.dart'; +import 'logging_controller.dart'; + +/// A controller for the log details view that provides search functionality. +class LogDetailsController extends DisposableController + with SearchControllerMixin, AutoDisposeControllerMixin { + LogDetailsController({required ValueListenable selectedLog}) { + init(); + addAutoDisposeListener(selectedLog, () { + _selectedLog = selectedLog.value; + refreshSearchMatches(); + }); + } + + LogData? _selectedLog; + + @override + List matchesForSearch( + String search, { + bool searchPreviousMatches = false, + }) { + if (search.isEmpty || _selectedLog == null) return []; + final matches = []; + + final text = _selectedLog!.prettyPrinted(); + if (text == null) return []; + + final regex = RegExp(search, caseSensitive: false); + final allMatches = regex.allMatches(text); + for (final match in allMatches) { + matches.add(LogDetailsMatch(match.start, match.end)); + } + return matches; + } + + @override + void dispose() { + _selectedLog = null; + super.dispose(); + } +} + +/// A search match in the log details view. +class LogDetailsMatch with SearchableDataMixin { + LogDetailsMatch(this.start, this.end); + + final int start; + final int end; + + Range get range => Range(start, end); + + @override + bool matchesSearchToken(RegExp regExpSearch) => false; +} diff --git a/packages/devtools_app/lib/src/screens/logging/logging_controller.dart b/packages/devtools_app/lib/src/screens/logging/logging_controller.dart index 8f9818c584a..fa93986930c 100644 --- a/packages/devtools_app/lib/src/screens/logging/logging_controller.dart +++ b/packages/devtools_app/lib/src/screens/logging/logging_controller.dart @@ -28,6 +28,7 @@ import '../../shared/primitives/utils.dart'; import '../../shared/ui/filter.dart'; import '../../shared/ui/search.dart'; import '../inspector/inspector_tree_controller.dart'; +import 'log_details_controller.dart'; import 'logging_screen.dart'; import 'metadata.dart'; @@ -110,6 +111,8 @@ class LoggingController extends DevToolsScreenController @override void init() { super.init(); + logDetailsController = LogDetailsController(selectedLog: selectedLog) + ..init(); addAutoDisposeListener(serviceConnection.serviceManager.connectedState, () { if (serviceConnection.serviceManager.connectedState.value.connected) { _handleConnectionStart(serviceConnection.serviceManager.service!); @@ -138,6 +141,7 @@ class LoggingController extends DevToolsScreenController @override void dispose() { + logDetailsController.dispose(); selectedLog.dispose(); unawaited(_logStatusController.close()); super.dispose(); @@ -234,6 +238,8 @@ class LoggingController extends DevToolsScreenController final _logStatusController = StreamController.broadcast(); + late final LogDetailsController logDetailsController; + List data = []; final selectedLog = ValueNotifier(null); diff --git a/packages/devtools_app/lib/src/screens/logging/logging_screen.dart b/packages/devtools_app/lib/src/screens/logging/logging_screen.dart index 2b68a9dc1fe..79d2c7cfbe2 100644 --- a/packages/devtools_app/lib/src/screens/logging/logging_screen.dart +++ b/packages/devtools_app/lib/src/screens/logging/logging_screen.dart @@ -87,7 +87,10 @@ class _LoggingScreenState extends State ValueListenableBuilder( valueListenable: controller.selectedLog, builder: (context, selected, _) { - return LogDetails(log: selected); + return LogDetails( + log: selected, + controller: controller.logDetailsController, + ); }, ), ], diff --git a/packages/devtools_app/lib/src/shared/table/_flat_table.dart b/packages/devtools_app/lib/src/shared/table/_flat_table.dart index 79bb3e7d15d..154868a0026 100644 --- a/packages/devtools_app/lib/src/shared/table/_flat_table.dart +++ b/packages/devtools_app/lib/src/shared/table/_flat_table.dart @@ -33,6 +33,7 @@ class SearchableFlatTable extends FlatTable { super.sizeColumnsToFit = true, super.rowHeight, super.selectionNotifier, + super.tallHeaders, }) : super( searchMatchesNotifier: searchController.searchMatches, activeSearchMatchNotifier: searchController.activeSearchMatch, diff --git a/packages/devtools_app/lib/src/shared/table/_table_row.dart b/packages/devtools_app/lib/src/shared/table/_table_row.dart index b7c6a010b92..afecd702bc7 100644 --- a/packages/devtools_app/lib/src/shared/table/_table_row.dart +++ b/packages/devtools_app/lib/src/shared/table/_table_row.dart @@ -263,7 +263,7 @@ class _TableRowState extends State> final box = SizedBox( height: widget._rowType == _TableRowType.data ? defaultRowHeight - : defaultHeaderHeight + (widget.tall ? densePadding : 0.0), + : defaultHeaderHeight + (widget.tall ? 2 * densePadding : 0.0), child: Material( color: _searchAwareBackgroundColor(), child: onPressed != null diff --git a/packages/devtools_app/lib/src/shared/ui/search_highlighter.dart b/packages/devtools_app/lib/src/shared/ui/search_highlighter.dart new file mode 100644 index 00000000000..6c93155edc8 --- /dev/null +++ b/packages/devtools_app/lib/src/shared/ui/search_highlighter.dart @@ -0,0 +1,143 @@ +// Copyright 2024 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +import 'package:devtools_app_shared/ui.dart'; +import 'package:flutter/material.dart'; + +import '../primitives/utils.dart'; + +/// A utility class for highlighting search matches in text. +extension SearchHighlighter on Never { + /// Highlights search matches in [text]. + static TextSpan highlight( + String text, + List matches, { + Range? activeMatch, + required TextStyle style, + }) { + if (matches.isEmpty) { + return TextSpan(text: text, style: style); + } + + final spans = []; + var lastIndex = 0; + for (final match in matches) { + final begin = match.begin.toInt(); + final end = match.end.toInt(); + if (begin > lastIndex) { + spans.add(TextSpan(text: text.substring(lastIndex, begin))); + } + + final isActive = activeMatch == match; + spans.add( + _searchAwareText( + text: text.substring(begin, end), + baseStyle: style, + isActive: isActive, + ), + ); + lastIndex = end; + } + + if (lastIndex < text.length) { + spans.add(TextSpan(text: text.substring(lastIndex))); + } + + return TextSpan(children: spans, style: style); + } + + /// Highlights search matches in a list of [TextSpan]s. + /// + /// This method handles matches that span across multiple [TextSpan]s. + static List highlightSpans( + List spans, { + required List matches, + Range? activeMatch, + required TextStyle style, + }) { + if (matches.isEmpty) return spans; + + final result = []; + var currentOffset = 0; + var matchIndex = 0; + + for (final span in spans) { + final spanText = span.toPlainText(); + final spanEnd = currentOffset + spanText.length; + + var lastSpanOffset = 0; + + while (matchIndex < matches.length) { + final match = matches[matchIndex]; + + // Match is after this span. + if (match.begin >= spanEnd) break; + + // Match ends before this span starts. + if (match.end <= currentOffset) { + matchIndex++; + continue; + } + + // Add leading un-highlighted text in this span. + final matchStartInSpan = (match.begin - currentOffset) + .clamp(0, spanText.length) + .toInt(); + if (matchStartInSpan > lastSpanOffset) { + result.add( + TextSpan( + text: spanText.substring(lastSpanOffset, matchStartInSpan), + style: span.style, + ), + ); + } + + // Add highlighted portion. + final matchEndInSpan = (match.end - currentOffset) + .clamp(0, spanText.length) + .toInt(); + final isActive = activeMatch == match; + result.add( + _searchAwareText( + text: spanText.substring(matchStartInSpan, matchEndInSpan), + baseStyle: span.style ?? style, + isActive: isActive, + ), + ); + + lastSpanOffset = matchEndInSpan; + + // If the match continues into the next span, don't increment matchIndex yet. + if (match.end > spanEnd) break; + + matchIndex++; + } + + // Add remaining un-highlighted text in this span. + if (lastSpanOffset < spanText.length) { + result.add( + TextSpan(text: spanText.substring(lastSpanOffset), style: span.style), + ); + } + + currentOffset = spanEnd; + } + + return result; + } +} + +TextSpan _searchAwareText({ + required String text, + required TextStyle baseStyle, + bool isActive = false, +}) { + return TextSpan( + text: text, + style: baseStyle.copyWith( + backgroundColor: isActive ? activeSearchMatchColor : searchMatchColor, + color: Colors.black, + ), + ); +} diff --git a/packages/devtools_app/macos/Runner.xcodeproj/project.pbxproj b/packages/devtools_app/macos/Runner.xcodeproj/project.pbxproj index 59aa8f360ec..ce7a071de3b 100644 --- a/packages/devtools_app/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/devtools_app/macos/Runner.xcodeproj/project.pbxproj @@ -21,14 +21,13 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ - 11595299B00138FF6A219878 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 46084CB9F244837191E61B73 /* Pods_RunnerTests.framework */; }; 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - E678665441E5C0F7F629BAD5 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5062035DDDD18FB35E98D5B6 /* Pods_Runner.framework */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -62,8 +61,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 11BB555C0F1767B9B5CB7CE0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 13053082F27293B7166BCBED /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; @@ -80,14 +77,9 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 46084CB9F244837191E61B73 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 5062035DDDD18FB35E98D5B6 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 68C587FFA5A0B8F46A0C5150 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - A7CE48BF63861DD9F3A9FA2F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - BDEB23F5F07C7F498EB77EA2 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - E11974409F5281249C10F0E1 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -95,7 +87,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 11595299B00138FF6A219878 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -103,7 +94,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E678665441E5C0F7F629BAD5 /* Pods_Runner.framework in Frameworks */, + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -136,8 +127,6 @@ 33CEB47122A05771004F2AC0 /* Flutter */, 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - 618DD25D42BF0C167E4D5128 /* Pods */, ); sourceTree = ""; }; @@ -164,6 +153,7 @@ 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, @@ -185,29 +175,6 @@ path = Runner; sourceTree = ""; }; - 618DD25D42BF0C167E4D5128 /* Pods */ = { - isa = PBXGroup; - children = ( - 11BB555C0F1767B9B5CB7CE0 /* Pods-Runner.debug.xcconfig */, - 68C587FFA5A0B8F46A0C5150 /* Pods-Runner.release.xcconfig */, - A7CE48BF63861DD9F3A9FA2F /* Pods-Runner.profile.xcconfig */, - E11974409F5281249C10F0E1 /* Pods-RunnerTests.debug.xcconfig */, - BDEB23F5F07C7F498EB77EA2 /* Pods-RunnerTests.release.xcconfig */, - 13053082F27293B7166BCBED /* Pods-RunnerTests.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 5062035DDDD18FB35E98D5B6 /* Pods_Runner.framework */, - 46084CB9F244837191E61B73 /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -215,7 +182,6 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 3E485AF46E5EF6A810E8A04C /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -234,13 +200,11 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - DC1C8B6797A659BE5B59B986 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - E765EEB3239836D35A4D4672 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -248,6 +212,9 @@ 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* devtools_app.app */; productType = "com.apple.product-type.application"; @@ -291,6 +258,9 @@ Base, ); mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -360,67 +330,6 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; - 3E485AF46E5EF6A810E8A04C /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - DC1C8B6797A659BE5B59B986 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - E765EEB3239836D35A4D4672 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -472,7 +381,6 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E11974409F5281249C10F0E1 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -487,7 +395,6 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BDEB23F5F07C7F498EB77EA2 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -502,7 +409,6 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 13053082F27293B7166BCBED /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -786,6 +692,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } diff --git a/packages/devtools_app/macos/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/devtools_app/macos/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000000..919434a6254 --- /dev/null +++ b/packages/devtools_app/macos/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/devtools_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/devtools_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index cadfa603a2e..559bf4e57a2 100644 --- a/packages/devtools_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/devtools_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + >(ListValueNotifier(data)); final mockLoggingController = MockLoggingController(); when(mockLoggingController.data).thenReturn(data); + final selectedLog = ValueNotifier(null); + when(mockLoggingController.selectedLog).thenReturn(selectedLog); + + final logDetailsController = LogDetailsController(selectedLog: selectedLog); when( - mockLoggingController.selectedLog, - ).thenReturn(ValueNotifier(null)); + mockLoggingController.logDetailsController, + ).thenReturn(logDetailsController); // Set up mock filter state. when(