diff --git a/packages/devtools_app/lib/src/debugger/debugger_controller.dart b/packages/devtools_app/lib/src/debugger/debugger_controller.dart index 8d570ecb4af..11934a5e794 100644 --- a/packages/devtools_app/lib/src/debugger/debugger_controller.dart +++ b/packages/devtools_app/lib/src/debugger/debugger_controller.dart @@ -1053,10 +1053,10 @@ class DebuggerController extends DisposableController // TODO(kenz): search through previous matches when possible. @override - List matchesForSearch( + Future> matchesForSearch( String search, { bool searchPreviousMatches = false, - }) { + }) async { if (search == null || search.isEmpty || parsedScript.value == null) { return []; } diff --git a/packages/devtools_app/lib/src/inspector/diagnostics.dart b/packages/devtools_app/lib/src/inspector/diagnostics.dart index f5333bb3757..d92bb907fc3 100644 --- a/packages/devtools_app/lib/src/inspector/diagnostics.dart +++ b/packages/devtools_app/lib/src/inspector/diagnostics.dart @@ -104,6 +104,7 @@ class DiagnosticsNodeDescription extends StatelessWidget { searchValue, textStyle, textStyle.merge(nodeDescriptionHighlightStyle), + addQuotes: true, ), ], ); @@ -185,6 +186,10 @@ class DiagnosticsNodeDescription extends StatelessWidget { TextStyle textStyle = baseStyle.merge(textStyleForLevel(diagnostic.level, colorScheme)); var descriptionTextStyle = textStyle; + final TextStyle searchMatchTextStyle = textStyle?.copyWith( + color: nodeDescriptionHighlightStyle?.color, + backgroundColor: nodeDescriptionHighlightStyle?.backgroundColor, + ); // TODO(jacobr): use TextSpans and SelectableText instead of Text. if (diagnostic.isProperty) { // Display of inline properties. @@ -192,7 +197,23 @@ class DiagnosticsNodeDescription extends StatelessWidget { final Map properties = diagnostic.valuePropertiesJson; if (name?.isNotEmpty == true && diagnostic.showName) { - children.add(Text('$name${diagnostic.separator} ', style: textStyle)); + children.add( + RichText( + maxLines: 1, + text: TextSpan( + children: [ + _buildHighlightedSearchPreview( + name, + searchValue, + textStyle, + searchMatchTextStyle, + ), + TextSpan(text: '${diagnostic.separator} '), + ], + ), + ), + ); + // provide some contrast between the name and description if both are // present. descriptionTextStyle = @@ -248,16 +269,30 @@ class DiagnosticsNodeDescription extends StatelessWidget { textStyle = textStyle..merge(inspector_text_styles.link(colorScheme)); } + final TextStyle descriptionSearchMatchTextStyle = + descriptionTextStyle?.copyWith( + color: nodeDescriptionHighlightStyle?.color, + backgroundColor: nodeDescriptionHighlightStyle?.backgroundColor, + ); + // TODO(jacobr): custom display for units, iterables, and padding. - children.add(Flexible( - child: buildDescription( - description, - descriptionTextStyle, - context, - colorScheme, - isProperty: true, + children.add( + Flexible( + child: RichText( + maxLines: 1, + text: TextSpan( + children: [ + _buildHighlightedSearchPreview( + description, + searchValue, + descriptionTextStyle, + descriptionSearchMatchTextStyle, + ), + ], + ), + ), ), - )); + ); if (diagnostic.level == DiagnosticLevel.fine && diagnostic.hasDefaultValue) { @@ -369,18 +404,19 @@ class DiagnosticsNodeDescription extends StatelessWidget { String textPreview, String searchValue, TextStyle textStyle, - TextStyle highlightTextStyle, - ) { + TextStyle highlightTextStyle, { + bool addQuotes = false, + }) { if (searchValue == null || searchValue.isEmpty) { return TextSpan( - text: '"$textPreview"', + text: addQuotes ? '"$textPreview"' : textPreview, style: textStyle, ); } if (textPreview.caseInsensitiveEquals(searchValue)) { return TextSpan( - text: '"$textPreview"', + text: addQuotes ? '"$textPreview"' : textPreview, style: highlightTextStyle, ); } @@ -388,13 +424,14 @@ class DiagnosticsNodeDescription extends StatelessWidget { final matches = searchValue.caseInsensitiveAllMatches(textPreview); if (matches.isEmpty) { return TextSpan( - text: '"$textPreview"', + text: addQuotes ? '"$textPreview"' : textPreview, style: textStyle, ); } final quoteSpan = TextSpan(text: '"', style: textStyle); - final spans = [quoteSpan]; + final spans = []; + if (addQuotes) spans.add(quoteSpan); var previousItemEnd = 0; for (final match in matches) { if (match.start > previousItemEnd) { @@ -416,7 +453,7 @@ class DiagnosticsNodeDescription extends StatelessWidget { text: textPreview.substring(previousItemEnd, textPreview.length), style: textStyle, )); - spans.add(quoteSpan); + if (addQuotes) spans.add(quoteSpan); return TextSpan(children: spans); } diff --git a/packages/devtools_app/lib/src/inspector/inspector_screen.dart b/packages/devtools_app/lib/src/inspector/inspector_screen.dart index ec2850860c1..e8e3628cc54 100644 --- a/packages/devtools_app/lib/src/inspector/inspector_screen.dart +++ b/packages/devtools_app/lib/src/inspector/inspector_screen.dart @@ -85,11 +85,8 @@ class InspectorScreenBodyState extends State /// Indicates whether search can be closed. The value is set to true when /// search target type dropdown is displayed - /// TODO(https://github.com/flutter/devtools/issues/3489) use this variable when adding the scope dropdown bool searchPreventClose = false; - SearchTargetType searchTarget = SearchTargetType.widget; - static const summaryTreeKey = Key('Summary Tree'); static const detailsTreeKey = Key('Details Tree'); static const minScreenWidthForTextBeforeScaling = 900.0; @@ -125,8 +122,6 @@ class InspectorScreenBodyState extends State onLayoutExplorerSupported: _onLayoutExplorerSupported, ); - summaryTreeController.setSearchTarget(searchTarget); - addAutoDisposeListener(searchFieldFocusNode, () { // Close the search once focus is lost and following conditions are met: // 1. Search string is empty. @@ -241,6 +236,24 @@ class InspectorScreenBodyState extends State shouldRequestFocus: searchVisible, supportsNavigation: true, onClose: _onSearchVisibleToggle, + prefix: ValueListenableBuilder( + valueListenable: summaryTreeController.searchTarget, + builder: (context, value, child) { + return DropdownButton( + underline: const SizedBox(), + value: value, + items: SearchTargetType.values.map((e) { + return DropdownMenuItem( + child: Text(e.name), + value: e, + ); + }).toList(), + onTap: () => searchPreventClose = !searchPreventClose, + onChanged: (value) => + summaryTreeController.setSearchTarget(value), + ); + }, + ), ), ), Expanded( diff --git a/packages/devtools_app/lib/src/inspector/inspector_tree.dart b/packages/devtools_app/lib/src/inspector/inspector_tree.dart index f9744c6ba51..83cd15797f3 100644 --- a/packages/devtools_app/lib/src/inspector/inspector_tree.dart +++ b/packages/devtools_app/lib/src/inspector/inspector_tree.dart @@ -313,12 +313,17 @@ class InspectorTreeConfig { enum SearchTargetType { widget, - // TODO(https://github.com/flutter/devtools/issues/3489) implement other search scopes: details, all etc + details, + all, } extension SearchTargetTypeExtension on SearchTargetType { String get name { switch (this) { + case SearchTargetType.details: + return 'Details'; + case SearchTargetType.all: + return 'All'; case SearchTargetType.widget: default: return 'Widget'; diff --git a/packages/devtools_app/lib/src/inspector/inspector_tree_controller.dart b/packages/devtools_app/lib/src/inspector/inspector_tree_controller.dart index 0c14137d912..7177e363020 100644 --- a/packages/devtools_app/lib/src/inspector/inspector_tree_controller.dart +++ b/packages/devtools_app/lib/src/inspector/inspector_tree_controller.dart @@ -75,10 +75,13 @@ class _InspectorTreeRowState extends State<_InspectorTreeRowWidget> controller: widget.inspectorTreeState.controller, scrollControllerX: widget.scrollControllerX, viewportWidth: widget.viewportWidth, + searchNotifier: + widget.inspectorTreeState.inspectorTreeController?.searchNotifier, onToggle: () { setExpanded(!isExpanded); }, debuggerController: widget.debuggerController, + isSummaryTree: widget.inspectorTreeState.isSummaryTree, ), ); } @@ -109,8 +112,6 @@ class InspectorTreeController extends Object InspectorTreeNode createNode() => InspectorTreeNode(); - SearchTargetType _searchTarget = SearchTargetType.widget; - void addClient(InspectorControllerClient value) { final firstClient = _clients.isEmpty; _clients.add(value); @@ -193,11 +194,17 @@ class InspectorTreeController extends Object /// * items are populated only when root is changed final _searchableCachedRows = []; - void setSearchTarget(SearchTargetType searchTarget) { - _searchTarget = searchTarget; + final _searchTarget = + ValueNotifier(SearchTargetType.widget); + + void setSearchTarget(SearchTargetType value) { + _searchTarget.value = value; + matchIndex.value = 0; refreshSearchMatches(); } + ValueListenable get searchTarget => _searchTarget; + // TODO: we should add a listener instead that clears the cache when the // root is marked as dirty. void _maybeClearCache() { @@ -615,26 +622,26 @@ class InspectorTreeController extends Object } @override - List matchesForSearch( - String search, { + Future> matchesForSearch(String search, { bool searchPreviousMatches = false, - }) { + }) async { final matches = []; - if (searchPreviousMatches) { - final previousMatches = searchMatches.value; - for (final previousMatch in previousMatches) { - if (previousMatch.node.diagnostic.searchValue - .caseInsensitiveContains(search)) { - matches.add(previousMatch); - } - } - - if (matches.isNotEmpty) return matches; - } - - int _debugStatsSearchOps = 0; - final _debugStatsWidgets = _searchableCachedRows.length; + // TODO fix this + // if (searchPreviousMatches) { + // final previousMatches = searchMatches.value; + // for (final previousMatch in previousMatches) { + // if (previousMatch.node.diagnostic.searchValue + // .caseInsensitiveContains(search)) { + // matches.add(previousMatch); + // } + // } + // + // if (matches.isNotEmpty) return matches; + // } + + final _debugStatsWidgetsTotal = _searchableCachedRows.length; + int _debugStatsRowsFound = 0; if (search == null || search.isEmpty || @@ -644,31 +651,66 @@ class InspectorTreeController extends Object return matches; } - debugPrint('Search started: ' + _searchTarget.toString()); + debugPrint('Search started: ${searchTarget.value}'); for (final row in _searchableCachedRows) { final diagnostic = row.node.diagnostic; if (row.node == null || diagnostic == null) continue; - // Widget search begin - if (_searchTarget == SearchTargetType.widget) { - _debugStatsSearchOps++; - if (diagnostic.searchValue.caseInsensitiveContains(search)) { - matches.add(row); - continue; - } + bool hasMatch = false; + switch (_searchTarget.value) { + case SearchTargetType.widget: + hasMatch = _hasWidgetMatch(diagnostic, search); + break; + case SearchTargetType.details: + hasMatch = await _hasPropertyMatch(diagnostic, search); + break; + case SearchTargetType.all: + hasMatch = _hasWidgetMatch(diagnostic, search) || + await _hasPropertyMatch(diagnostic, search); + break; + } + + if (hasMatch) { + _debugStatsRowsFound++; + matches.add(row); } - // Widget search end } - debugPrint('Search completed with ' + - _debugStatsWidgets.toString() + - ' widgets, ' + - _debugStatsSearchOps.toString() + - ' ops'); + debugPrint('Search completed. ' + 'Found $_debugStatsRowsFound rows. ' + 'Total rows is $_debugStatsWidgetsTotal'); return matches; } + + bool _hasWidgetMatch(RemoteDiagnosticsNode diagnostic, String search) { + return diagnostic.searchValue.caseInsensitiveContains(search); + } + + Future _hasPropertyMatch( + RemoteDiagnosticsNode diagnostic, + String search, + ) async { + List properties = diagnostic.inlineProperties; + if (diagnostic.inlineProperties?.isNotEmpty != true) { + final service = diagnostic.inspectorService; + if (!service.disposed) { + properties = await diagnostic.getProperties(service); + } + } + + if (properties == null) return false; + + for (final property in properties) { + if (property.level == DiagnosticLevel.hidden) continue; + if (property.name.caseInsensitiveContains(search) || + property.description?.caseInsensitiveContains(search) == true) { + return true; + } + } + return false; + } } extension RemoteDiagnosticsNodeExtension on RemoteDiagnosticsNode { @@ -719,6 +761,9 @@ class _InspectorTreeState extends State implements InspectorControllerClient { InspectorTreeController get controller => widget.controller; + InspectorTreeController get inspectorTreeController => + widget.inspectorTreeController; + bool get isSummaryTree => widget.isSummaryTree; ScrollController _scrollControllerY; @@ -1126,10 +1171,12 @@ class InspectorRowContent extends StatelessWidget { @required this.controller, @required this.onToggle, @required this.expandArrowAnimation, + @required this.searchNotifier, this.error, @required this.scrollControllerX, @required this.viewportWidth, @required this.debuggerController, + @required this.isSummaryTree, }); final InspectorTreeRow row; @@ -1139,6 +1186,8 @@ class InspectorRowContent extends StatelessWidget { final Animation expandArrowAnimation; final ScrollController scrollControllerX; final double viewportWidth; + final ValueListenable searchNotifier; + final bool isSummaryTree; /// A [DevToolsError] that applies to the widget in this row. /// @@ -1170,10 +1219,16 @@ class InspectorRowContent extends StatelessWidget { Widget rowWidget = Padding( padding: EdgeInsets.only(left: currentX), child: ValueListenableBuilder( - valueListenable: controller.searchNotifier, + // TODO find better solution than passing searchNotifier here + valueListenable: searchNotifier ?? controller.searchNotifier, builder: (context, searchValue, _) { return Opacity( - opacity: searchValue.isEmpty || row.isSearchMatch ? 1 : 0.2, + opacity: !isSummaryTree || + node.isProperty || + searchValue.isEmpty || + row.isSearchMatch + ? 1 + : 0.2, child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -1216,11 +1271,9 @@ class InspectorRowContent extends StatelessWidget { errorText: error?.errorMessage, debuggerController: debuggerController, nodeDescriptionHighlightStyle: - searchValue.isEmpty || !row.isSearchMatch - ? inspector_text_styles.regular - : row.isSelected - ? theme.searchMatchHighlightStyleFocused - : theme.searchMatchHighlightStyle, + row.isSelected || !isSummaryTree + ? theme.searchMatchHighlightStyleFocused + : theme.searchMatchHighlightStyle, ), ), ), diff --git a/packages/devtools_app/lib/src/logging/logging_controller.dart b/packages/devtools_app/lib/src/logging/logging_controller.dart index 615c14ebf01..ee27ef05200 100644 --- a/packages/devtools_app/lib/src/logging/logging_controller.dart +++ b/packages/devtools_app/lib/src/logging/logging_controller.dart @@ -601,10 +601,10 @@ class LoggingController extends DisposableController // TODO(kenz): search through previous matches when possible. @override - List matchesForSearch( + Future> matchesForSearch( String search, { bool searchPreviousMatches = false, - }) { + }) async { if (search == null || search.isEmpty) return []; final matches = []; final caseInsensitiveSearch = search.toLowerCase(); diff --git a/packages/devtools_app/lib/src/network/network_controller.dart b/packages/devtools_app/lib/src/network/network_controller.dart index ec2032f4382..09512b3056a 100644 --- a/packages/devtools_app/lib/src/network/network_controller.dart +++ b/packages/devtools_app/lib/src/network/network_controller.dart @@ -356,10 +356,10 @@ class NetworkController // TODO(kenz): search through previous matches when possible. @override - List matchesForSearch( + Future> matchesForSearch( String search, { bool searchPreviousMatches = false, - }) { + }) async { if (search == null || search.isEmpty) return []; final matches = []; final caseInsensitiveSearch = search.toLowerCase(); diff --git a/packages/devtools_app/lib/src/performance/legacy/performance_controller.dart b/packages/devtools_app/lib/src/performance/legacy/performance_controller.dart index a12e2dfa886..fc33f22a497 100644 --- a/packages/devtools_app/lib/src/performance/legacy/performance_controller.dart +++ b/packages/devtools_app/lib/src/performance/legacy/performance_controller.dart @@ -468,10 +468,10 @@ class LegacyPerformanceController } @override - List matchesForSearch( + Future> matchesForSearch( String search, { bool searchPreviousMatches = false, - }) { + }) async { if (search?.isEmpty ?? true) return []; final matches = []; for (final event in data.timelineEvents) { diff --git a/packages/devtools_app/lib/src/performance/performance_controller.dart b/packages/devtools_app/lib/src/performance/performance_controller.dart index 98958f06b2b..8d9b72c5eb1 100644 --- a/packages/devtools_app/lib/src/performance/performance_controller.dart +++ b/packages/devtools_app/lib/src/performance/performance_controller.dart @@ -809,10 +809,10 @@ class PerformanceController extends DisposableController } @override - List matchesForSearch( + Future> matchesForSearch( String search, { bool searchPreviousMatches = false, - }) { + }) async { if (search?.isEmpty ?? true) return []; final matches = []; if (searchPreviousMatches) { diff --git a/packages/devtools_app/lib/src/profiler/cpu_profile_controller.dart b/packages/devtools_app/lib/src/profiler/cpu_profile_controller.dart index 1f5acecd8ce..3ba49b90645 100644 --- a/packages/devtools_app/lib/src/profiler/cpu_profile_controller.dart +++ b/packages/devtools_app/lib/src/profiler/cpu_profile_controller.dart @@ -242,10 +242,10 @@ class CpuProfilerController // TODO(kenz): search through previous matches when possible. @override - List matchesForSearch( + Future> matchesForSearch( String search, { bool searchPreviousMatches = false, - }) { + }) async { if (search?.isEmpty ?? true) return []; final regexSearch = RegExp(search, caseSensitive: false); final matches = []; diff --git a/packages/devtools_app/lib/src/ui/search.dart b/packages/devtools_app/lib/src/ui/search.dart index 4681eaf41c6..41c8093cecf 100644 --- a/packages/devtools_app/lib/src/ui/search.dart +++ b/packages/devtools_app/lib/src/ui/search.dart @@ -59,7 +59,9 @@ mixin SearchControllerMixin { ValueListenable> get searchMatches => _searchMatches; - void refreshSearchMatches({bool searchPreviousMatches = false}) { + Future refreshSearchMatches({ + bool searchPreviousMatches = false, + }) async { if (_searchNotifier.value != null && _searchNotifier.value.isNotEmpty) { if (debounceDelay != null) { _startDebounceTimer( @@ -67,7 +69,7 @@ mixin SearchControllerMixin { searchPreviousMatches: searchPreviousMatches, ); } else { - final matches = matchesForSearch( + final matches = await matchesForSearch( _searchNotifier.value, searchPreviousMatches: searchPreviousMatches, ); @@ -100,12 +102,10 @@ mixin SearchControllerMixin { searchInProgress = true; // Start new search operation - final future = Future(() { - return matchesForSearch( - _searchNotifier.value, - searchPreviousMatches: searchPreviousMatches, - ); - }).then((matches) { + final future = matchesForSearch( + _searchNotifier.value, + searchPreviousMatches: searchPreviousMatches, + ).then((matches) { searchInProgress = false; _updateMatches(matches); }); @@ -181,10 +181,10 @@ mixin SearchControllerMixin { /// Duration.zero (default) disables debounce Duration get debounceDelay => null; - List matchesForSearch( + Future> matchesForSearch( String search, { bool searchPreviousMatches = false, - }) => + }) async => []; /// Called when selected match index changes. Index is 0 based diff --git a/packages/devtools_app/test/goldens/memory_chart_fixed_all.png b/packages/devtools_app/test/goldens/memory_chart_fixed_all.png index 125c5948ede..ba37d9adcd3 100644 Binary files a/packages/devtools_app/test/goldens/memory_chart_fixed_all.png and b/packages/devtools_app/test/goldens/memory_chart_fixed_all.png differ diff --git a/packages/devtools_app/test/goldens/memory_chart_fixed_five_minutes.png b/packages/devtools_app/test/goldens/memory_chart_fixed_five_minutes.png index 7ceeba9308a..ad9a65c147c 100644 Binary files a/packages/devtools_app/test/goldens/memory_chart_fixed_five_minutes.png and b/packages/devtools_app/test/goldens/memory_chart_fixed_five_minutes.png differ diff --git a/packages/devtools_app/test/goldens/memory_chart_fixed_live.png b/packages/devtools_app/test/goldens/memory_chart_fixed_live.png index 4b2ada184aa..d23646a90e3 100644 Binary files a/packages/devtools_app/test/goldens/memory_chart_fixed_live.png and b/packages/devtools_app/test/goldens/memory_chart_fixed_live.png differ diff --git a/packages/devtools_app/test/goldens/memory_chart_scaled_all.png b/packages/devtools_app/test/goldens/memory_chart_scaled_all.png index 79c36808ca0..1f6a7ee684d 100644 Binary files a/packages/devtools_app/test/goldens/memory_chart_scaled_all.png and b/packages/devtools_app/test/goldens/memory_chart_scaled_all.png differ diff --git a/packages/devtools_app/test/goldens/memory_chart_scaled_five_minute.png b/packages/devtools_app/test/goldens/memory_chart_scaled_five_minute.png index 6735d76b50e..885527c1701 100644 Binary files a/packages/devtools_app/test/goldens/memory_chart_scaled_five_minute.png and b/packages/devtools_app/test/goldens/memory_chart_scaled_five_minute.png differ diff --git a/packages/devtools_app/test/goldens/memory_chart_scaled_live.png b/packages/devtools_app/test/goldens/memory_chart_scaled_live.png index 66002f5fdfc..0c9c1b32f2c 100644 Binary files a/packages/devtools_app/test/goldens/memory_chart_scaled_live.png and b/packages/devtools_app/test/goldens/memory_chart_scaled_live.png differ diff --git a/packages/devtools_app/test/goldens/memory_heap_android.png b/packages/devtools_app/test/goldens/memory_heap_android.png index ee04d302b57..256f9a4a6bb 100644 Binary files a/packages/devtools_app/test/goldens/memory_heap_android.png and b/packages/devtools_app/test/goldens/memory_heap_android.png differ diff --git a/packages/devtools_app/test/goldens/memory_heap_tree.png b/packages/devtools_app/test/goldens/memory_heap_tree.png index d5569ceb24f..ef15c216412 100644 Binary files a/packages/devtools_app/test/goldens/memory_heap_tree.png and b/packages/devtools_app/test/goldens/memory_heap_tree.png differ diff --git a/packages/devtools_app/test/goldens/memory_hover_card.png b/packages/devtools_app/test/goldens/memory_hover_card.png index c631acabd23..17284c5aa16 100644 Binary files a/packages/devtools_app/test/goldens/memory_hover_card.png and b/packages/devtools_app/test/goldens/memory_hover_card.png differ