Add support for searching within the log details view (#9712)
diff --git a/packages/devtools_app/lib/devtools_app.dart b/packages/devtools_app/lib/devtools_app.dart
index ddfa746..5917216 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_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 19ca7e4..d5f1ebd 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/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 @@
return null;
}
- List<InlineSpan> _contentsWithMatch(
- List<InlineSpan> startingContents,
- SourceToken match,
- Color matchColor, {
- required BuildContext context,
- }) {
- final contentsWithMatch = <InlineSpan>[];
- 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<TextSpan>(),
+ matches: searchMatches?.map((m) => m.range).toList() ?? [],
+ activeMatch: activeSearchMatch?.range,
+ style: theme.regularTextStyle,
+ ),
style: lineContents.style,
);
}
-
- List<InlineSpan>? _activeSearchAwareLineContents(
- List<InlineSpan> startingContents, {
- required BuildContext context,
- }) {
- final match = activeSearchMatch;
- if (match == null) return startingContents;
- return _contentsWithMatch(
- startingContents,
- match,
- activeSearchMatchColor,
- context: context,
- );
- }
-
- List<InlineSpan> _searchMatchAwareLineContents(
- List<InlineSpan> startingContents, {
- required BuildContext context,
- }) {
- final matches = searchMatches;
- if (matches == null || matches.isEmpty) return startingContents;
- final searchMatchesToFind = List<SourceToken>.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 12affb7..f69e302 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 @@
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 977af41..f277c54 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 '../../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<LogDetails> createState() => _LogDetailsState();
@@ -45,6 +49,10 @@
if (widget.log != oldWidget.log) {
unawaited(_computeLogDetails());
}
+ if (widget.controller != oldWidget.controller) {
+ cancelListeners();
+ addAutoDisposeListener(preferences.logging.detailsFormat);
+ }
}
Future<void> _computeLogDetails() async {
@@ -81,6 +89,7 @@
header: _LogDetailsHeader(
log: log,
format: preferences.logging.detailsFormat.value,
+ controller: widget.controller,
),
child: Scrollbar(
controller: scrollController,
@@ -93,9 +102,9 @@
? 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 _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 @@
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 @@
}
}
+/// 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<LogDetailsController>(
+ 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<LogDetailsMatch>)
+ .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 0475919..c406a42 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 @@
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 0000000..33eec93
--- /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<LogDetailsMatch>, AutoDisposeControllerMixin {
+ LogDetailsController({required ValueListenable<LogData?> selectedLog}) {
+ init();
+ addAutoDisposeListener(selectedLog, () {
+ _selectedLog = selectedLog.value;
+ refreshSearchMatches();
+ });
+ }
+
+ LogData? _selectedLog;
+
+ @override
+ List<LogDetailsMatch> matchesForSearch(
+ String search, {
+ bool searchPreviousMatches = false,
+ }) {
+ if (search.isEmpty || _selectedLog == null) return [];
+ final matches = <LogDetailsMatch>[];
+
+ 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 8f9818c..fa93986 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/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 @@
@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 @@
@override
void dispose() {
+ logDetailsController.dispose();
selectedLog.dispose();
unawaited(_logStatusController.close());
super.dispose();
@@ -234,6 +238,8 @@
final _logStatusController = StreamController<String>.broadcast();
+ late final LogDetailsController logDetailsController;
+
List<LogData> data = <LogData>[];
final selectedLog = ValueNotifier<LogData?>(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 2b68a9d..79d2c7c 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 @@
ValueListenableBuilder<LogData?>(
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 79bb3e7..154868a 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 @@
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 b7c6a01..afecd70 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 @@
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 0000000..6c93155
--- /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<Range> matches, {
+ Range? activeMatch,
+ required TextStyle style,
+ }) {
+ if (matches.isEmpty) {
+ return TextSpan(text: text, style: style);
+ }
+
+ final spans = <TextSpan>[];
+ 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<InlineSpan> highlightSpans(
+ List<TextSpan> spans, {
+ required List<Range> matches,
+ Range? activeMatch,
+ required TextStyle style,
+ }) {
+ if (matches.isEmpty) return spans;
+
+ final result = <InlineSpan>[];
+ 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 59aa8f3..ce7a071 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 = "<group>"; };
- 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 = "<group>"; };
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 = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
@@ -80,14 +77,9 @@
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
- 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 = "<group>"; };
+ 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
- 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 = "<group>"; };
- 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 = "<group>"; };
- 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 = "<group>"; };
/* 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 = "<group>";
};
@@ -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 = "<group>";
};
- 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 = "<group>";
- };
- D73912EC22F37F3D000D13A0 /* Frameworks */ = {
- isa = PBXGroup;
- children = (
- 5062035DDDD18FB35E98D5B6 /* Pods_Runner.framework */,
- 46084CB9F244837191E61B73 /* Pods_RunnerTests.framework */,
- );
- name = Frameworks;
- sourceTree = "<group>";
- };
/* 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 0000000..919434a
--- /dev/null
+++ b/packages/devtools_app/macos/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "self:">
+ </FileRef>
+</Workspace>
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 cadfa60..559bf4e 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 @@
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
+ <PreActions>
+ <ExecutionAction
+ ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
+ <ActionContent
+ title = "Run Prepare Flutter Framework Script"
+ scriptText = ""$FLUTTER_ROOT"/packages/flutter_tools/bin/macos_assemble.sh prepare ">
+ <EnvironmentBuildable>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "33CC10EC2044A3C60003C045"
+ BuildableName = "devtools_app.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </EnvironmentBuildable>
+ </ActionContent>
+ </ExecutionAction>
+ </PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
index 26d92bf..6893cc7 100644
--- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
+++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
@@ -11,7 +11,7 @@
The 2.57.0 release of the Dart and Flutter DevTools
includes the following changes among other general improvements.
To learn more about DevTools, check out the
-[DevTools overview](/tools/devtools).
+[DevTools overview](https://docs.flutter.dev/tools/devtools).
## General updates
@@ -43,7 +43,8 @@
## Logging updates
-TODO: Remove this section if there are not any updates.
+- Added support for searching within the log details view (raw text mode). [#9712](https://github.com/flutter/devtools/pull/9712)
+ 
## App size tool updates
diff --git a/packages/devtools_app/release_notes/images/log_details_search.png b/packages/devtools_app/release_notes/images/log_details_search.png
new file mode 100644
index 0000000..a99d6e3
--- /dev/null
+++ b/packages/devtools_app/release_notes/images/log_details_search.png
Binary files differ
diff --git a/packages/devtools_app/test/screens/logging/logging_screen_data_test.dart b/packages/devtools_app/test/screens/logging/logging_screen_data_test.dart
index 8852ab0..ab38883 100644
--- a/packages/devtools_app/test/screens/logging/logging_screen_data_test.dart
+++ b/packages/devtools_app/test/screens/logging/logging_screen_data_test.dart
@@ -217,16 +217,14 @@
const index = 9;
bool containsJson(Widget widget) {
if (widget is! Text) return false;
- final content = (widget.data ?? '').trim();
+ final content = (widget.data ?? widget.textSpan?.toPlainText() ?? '')
+ .trim();
return content.startsWith('{') &&
content.endsWith('}') &&
content != '{ }';
}
- final findJson = find.descendant(
- of: find.byType(LogDetails),
- matching: find.byWidgetPredicate(containsJson),
- );
+ final findJson = find.byWidgetPredicate(containsJson);
await pumpLoggingScreen(tester);
await tester.pumpAndSettle();
diff --git a/packages/devtools_app/test/shared/ui/search_highlighter_test.dart b/packages/devtools_app/test/shared/ui/search_highlighter_test.dart
new file mode 100644
index 0000000..00b172d
--- /dev/null
+++ b/packages/devtools_app/test/shared/ui/search_highlighter_test.dart
@@ -0,0 +1,121 @@
+// 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/src/shared/primitives/utils.dart';
+import 'package:devtools_app/src/shared/ui/search_highlighter.dart';
+import 'package:devtools_app_shared/ui.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ const style = TextStyle(fontSize: 12.0);
+
+ group('SearchHighlighter.highlight', () {
+ test('no matches', () {
+ final result = SearchHighlighter.highlight(
+ 'Hello World',
+ [],
+ style: style,
+ );
+ expect(result.text, 'Hello World');
+ expect(result.children, isNull);
+ });
+
+ test('single match', () {
+ final result = SearchHighlighter.highlight('Hello World', [
+ const Range(0, 5),
+ ], style: style);
+ expect(result.children!.length, 2);
+ expect(result.children![0].toPlainText(), 'Hello');
+ expect(result.children![0].style!.backgroundColor, searchMatchColor);
+ expect(result.children![1].toPlainText(), ' World');
+ });
+
+ test('multiple matches with active match', () {
+ final matches = [const Range(0, 5), const Range(6, 11)];
+ final result = SearchHighlighter.highlight(
+ 'Hello World',
+ matches,
+ activeMatch: matches[1],
+ style: style,
+ );
+ expect(result.children!.length, 3);
+ expect(result.children![0].toPlainText(), 'Hello');
+ expect(result.children![0].style!.backgroundColor, searchMatchColor);
+ expect(result.children![1].toPlainText(), ' ');
+ expect(result.children![2].toPlainText(), 'World');
+ expect(
+ result.children![2].style!.backgroundColor,
+ activeSearchMatchColor,
+ );
+ });
+ });
+
+ group('SearchHighlighter.highlightSpans', () {
+ test('no matches', () {
+ final spans = [
+ const TextSpan(text: 'Hello ', style: style),
+ const TextSpan(text: 'World', style: style),
+ ];
+ final result = SearchHighlighter.highlightSpans(
+ spans,
+ matches: [],
+ style: style,
+ );
+ expect(result, spans);
+ });
+
+ test('match across spans', () {
+ final spans = [
+ const TextSpan(text: 'Hello ', style: style),
+ const TextSpan(text: 'World', style: style),
+ ];
+ // "o Wor"
+ final matches = [const Range(4, 9)];
+ final result = SearchHighlighter.highlightSpans(
+ spans,
+ matches: matches,
+ activeMatch: matches[0],
+ style: style,
+ );
+
+ expect(result.length, 4);
+ expect(result[0].toPlainText(), 'Hell');
+ expect(result[1].toPlainText(), 'o ');
+ expect(result[1].style!.backgroundColor, activeSearchMatchColor);
+ expect(result[2].toPlainText(), 'Wor');
+ expect(result[2].style!.backgroundColor, activeSearchMatchColor);
+ expect(result[3].toPlainText(), 'ld');
+ });
+
+ test('multiple matches across multiple spans', () {
+ final spans = [
+ const TextSpan(text: 'abc', style: style),
+ const TextSpan(text: 'def', style: style),
+ const TextSpan(text: 'ghi', style: style),
+ ];
+ // "bcd", "fgh"
+ final matches = [const Range(1, 4), const Range(5, 8)];
+ final result = SearchHighlighter.highlightSpans(
+ spans,
+ matches: matches,
+ activeMatch: matches[1],
+ style: style,
+ );
+
+ expect(result.length, 7);
+ expect(result[0].toPlainText(), 'a');
+ expect(result[1].toPlainText(), 'bc'); // match 1 part 1
+ expect(result[1].style!.backgroundColor, searchMatchColor);
+ expect(result[2].toPlainText(), 'd'); // match 1 part 2
+ expect(result[2].style!.backgroundColor, searchMatchColor);
+ expect(result[3].toPlainText(), 'e');
+ expect(result[4].toPlainText(), 'f'); // match 2 part 1
+ expect(result[4].style!.backgroundColor, activeSearchMatchColor);
+ expect(result[5].toPlainText(), 'gh'); // match 2 part 2
+ expect(result[5].style!.backgroundColor, activeSearchMatchColor);
+ expect(result[6].toPlainText(), 'i');
+ });
+ });
+}
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_empty1.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_empty1.png
index 869ff8e..9b618a3 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_empty1.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_empty1.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_empty2.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_empty2.png
index 869ff8e..9b618a3 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_empty2.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_empty2.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_selected_class.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_selected_class.png
index 67d1d9f..5f17de5 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_selected_class.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_selected_class.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_except_diff.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_except_diff.png
index b34779c..d2cc323 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_except_diff.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_except_diff.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_except_single.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_except_single.png
index cf36edd..a3bcbb4 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_except_single.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_except_single.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_scene_diff.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_scene_diff.png
index b34779c..d2cc323 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_scene_diff.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_scene_diff.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_scene_single.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_scene_single.png
index cf36edd..a3bcbb4 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_scene_single.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_scene_single.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_showAll_diff.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_showAll_diff.png
index b34779c..d2cc323 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_showAll_diff.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_showAll_diff.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_showAll_single.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_showAll_single.png
index cf36edd..a3bcbb4 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_showAll_single.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_showAll_single.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_three_snapshots1.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_three_snapshots1.png
index 67d1d9f..5f17de5 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_three_snapshots1.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_three_snapshots1.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_three_snapshots2.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_three_snapshots2.png
index 67d1d9f..5f17de5 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_three_snapshots2.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_three_snapshots2.png
Binary files differ
diff --git a/packages/devtools_app_shared/lib/src/ui/theme/theme.dart b/packages/devtools_app_shared/lib/src/ui/theme/theme.dart
index 3513a22..07c1bf1 100644
--- a/packages/devtools_app_shared/lib/src/ui/theme/theme.dart
+++ b/packages/devtools_app_shared/lib/src/ui/theme/theme.dart
@@ -278,6 +278,7 @@
const extraWideSearchFieldWidth = 600.0;
const wideSearchFieldWidth = 400.0;
+const mediumSearchFieldWidth = 300.0;
const defaultSearchFieldWidth = 200.0;
const defaultTextFieldHeight = 26.0;
diff --git a/packages/devtools_test/lib/src/mocks/generated_mocks_factories.dart b/packages/devtools_test/lib/src/mocks/generated_mocks_factories.dart
index f710b85..f16f273 100644
--- a/packages/devtools_test/lib/src/mocks/generated_mocks_factories.dart
+++ b/packages/devtools_test/lib/src/mocks/generated_mocks_factories.dart
@@ -196,9 +196,13 @@
provideDummy<ListValueNotifier<LogData>>(ListValueNotifier<LogData>(data));
final mockLoggingController = MockLoggingController();
when(mockLoggingController.data).thenReturn(data);
+ final selectedLog = ValueNotifier<LogData?>(null);
+ when(mockLoggingController.selectedLog).thenReturn(selectedLog);
+
+ final logDetailsController = LogDetailsController(selectedLog: selectedLog);
when(
- mockLoggingController.selectedLog,
- ).thenReturn(ValueNotifier<LogData?>(null));
+ mockLoggingController.logDetailsController,
+ ).thenReturn(logDetailsController);
// Set up mock filter state.
when(