diff --git a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart index 5f8a493a845..03c2edce793 100644 --- a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart +++ b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart @@ -9,10 +9,12 @@ import '../../../shared/analytics/analytics.dart' as ga; import '../../../shared/analytics/constants.dart' as gac; import '../../../shared/editor/api_classes.dart'; import '../../../shared/editor/editor_client.dart'; +import '../../../shared/ui/filter.dart'; import '../../../shared/utils/utils.dart'; +import 'property_editor_types.dart'; typedef EditableWidgetData = - ({List args, String? name, String? documentation}); + ({List properties, String? name, String? documentation}); typedef EditArgumentFunction = Future Function({ @@ -21,7 +23,7 @@ typedef EditArgumentFunction = }); class PropertyEditorController extends DisposableController - with AutoDisposeControllerMixin { + with AutoDisposeControllerMixin, FilterControllerMixin { PropertyEditorController(this.editorClient) { init(); } @@ -46,6 +48,7 @@ class PropertyEditorController extends DisposableController super.init(); _editableArgsDebouncer = Debouncer(duration: _editableArgsDebounceDuration); + // Update in response to ActiveLocationChanged events. autoDisposeStreamSubscription( editorClient.activeLocationChangedStream.listen((event) async { final textDocument = event.textDocument; @@ -81,6 +84,17 @@ class PropertyEditorController extends DisposableController super.dispose(); } + @override + void filterData(Filter filter) { + super.filterData(filter); + final filtered = (_editableWidgetData.value?.properties ?? []).where( + (property) => property.matchesQuery(filter.queryFilter.query), + ); + filteredData + ..clear() + ..addAll(filtered); + } + Future editArgument({ required String name, required T value, @@ -107,13 +121,18 @@ class PropertyEditorController extends DisposableController textDocument: textDocument, position: cursorPosition, ); - final args = result?.args ?? []; + final properties = + (result?.args ?? []) + .map(argToProperty) + .nonNulls + .toList(); final name = result?.name; _editableWidgetData.value = ( - args: args, + properties: properties, name: name, documentation: result?.documentation, ); + filterData(activeFilter.value); // Register impression. ga.impression( gaId, @@ -127,9 +146,11 @@ class PropertyEditorController extends DisposableController TextDocument? document, CursorPosition? cursorPosition, }) { + setActiveFilter(); if (editableArgsResult != null) { _editableWidgetData.value = ( - args: editableArgsResult.args, + properties: + editableArgsResult.args.map(argToProperty).nonNulls.toList(), name: editableArgsResult.name, documentation: editableArgsResult.documentation, ); @@ -140,5 +161,6 @@ class PropertyEditorController extends DisposableController if (cursorPosition != null) { _currentCursorPosition = cursorPosition; } + filterData(activeFilter.value); } } diff --git a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_types.dart b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_types.dart index f212ec63ccc..39b3a634703 100644 --- a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_types.dart +++ b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_types.dart @@ -7,6 +7,7 @@ import 'package:devtools_app_shared/utils.dart'; import 'package:meta/meta.dart'; import '../../../shared/editor/api_classes.dart'; +import '../../../shared/primitives/utils.dart'; /// Record representing an option for an [EditableProperty]. typedef PropertyOption = ({String text, bool isDefault}); @@ -167,6 +168,13 @@ class EditableProperty extends EditableArgument { Object? convertFromInputString(String? _) { throw UnimplementedError(); } + + bool matchesQuery(String query) { + final regExpQuery = RegExp(query, caseSensitive: false); + return name.caseInsensitiveContains(regExpQuery) || + valueDisplay.caseInsensitiveContains(regExpQuery) || + type.caseInsensitiveContains(regExpQuery); + } } mixin NumericProperty on EditableProperty { diff --git a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_view.dart b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_view.dart index 3db138d8e9e..c7dfe0317de 100644 --- a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_view.dart +++ b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_view.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import '../../../shared/primitives/utils.dart'; import '../../../shared/ui/common_widgets.dart'; +import '../../../shared/ui/filter.dart'; import 'property_editor_controller.dart'; import 'property_editor_inputs.dart'; import 'property_editor_types.dart'; @@ -25,6 +26,7 @@ class PropertyEditorView extends StatelessWidget { controller.editorClient.editArgumentMethodName, controller.editorClient.editableArgumentsMethodName, controller.editableWidgetData, + controller.filteredData, ], builder: (_, values, _) { final editArgumentMethodName = values.first as String?; @@ -42,7 +44,8 @@ class PropertyEditorView extends StatelessWidget { ); } - final (:args, :name, :documentation) = editableWidgetData; + final filteredProperties = values.fourth as List; + final (:properties, :name, :documentation) = editableWidgetData; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -51,11 +54,11 @@ class PropertyEditorView extends StatelessWidget { name: name, documentation: documentation, ), - args.isEmpty + properties.isEmpty ? _NoEditablePropertiesMessage(name: name) : _PropertiesList( - editableProperties: args.map(argToProperty).nonNulls.toList(), - editProperty: controller.editArgument, + controller: controller, + editableProperties: filteredProperties, ), ], ); @@ -66,12 +69,12 @@ class PropertyEditorView extends StatelessWidget { class _PropertiesList extends StatefulWidget { const _PropertiesList({ + required this.controller, required this.editableProperties, - required this.editProperty, }); + final PropertyEditorController controller; final List editableProperties; - final EditArgumentFunction editProperty; static const defaultItemPadding = borderPadding; static const denseItemPadding = defaultItemPadding / 2; @@ -99,10 +102,13 @@ class _PropertiesListState extends State<_PropertiesList> { Widget build(BuildContext context) { return Column( children: [ + _FilterControls(controller: widget.controller), + if (widget.editableProperties.isEmpty) + const _NoMatchingPropertiesMessage(), for (final property in widget.editableProperties) _EditablePropertyItem( property: property, - editProperty: widget.editProperty, + editProperty: widget.controller.editArgument, ), ].joinWith(const PaddedDivider.noPadding()), ); @@ -142,6 +148,29 @@ class _EditablePropertyItem extends StatelessWidget { } } +class _FilterControls extends StatelessWidget { + const _FilterControls({required this.controller}); + + final PropertyEditorController controller; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(_PropertiesList.defaultItemPadding), + child: Row( + children: [ + Expanded( + child: StandaloneFilterField( + controller: controller, + filteredItem: 'property', + ), + ), + ], + ), + ); + } +} + class _PropertyLabels extends StatelessWidget { const _PropertyLabels({required this.property}); @@ -289,6 +318,15 @@ class _NoEditablePropertiesMessage extends StatelessWidget { } } +class _NoMatchingPropertiesMessage extends StatelessWidget { + const _NoMatchingPropertiesMessage(); + + @override + Widget build(BuildContext context) { + return const Text('No properties matching the current filter.'); + } +} + class _WidgetNameAndDocumentation extends StatelessWidget { const _WidgetNameAndDocumentation({required this.name, this.documentation}); @@ -320,7 +358,7 @@ class _WidgetNameAndDocumentation extends StatelessWidget { ), ], ), - const PaddedDivider(), + const PaddedDivider.noPadding(), ], ); } diff --git a/packages/devtools_app/test/standalone_ui/ide_shared/property_editor/property_editor_test.dart b/packages/devtools_app/test/standalone_ui/ide_shared/property_editor/property_editor_test.dart index 99b9a9ae087..e86c0c850bd 100644 --- a/packages/devtools_app/test/standalone_ui/ide_shared/property_editor/property_editor_test.dart +++ b/packages/devtools_app/test/standalone_ui/ide_shared/property_editor/property_editor_test.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:devtools_app/devtools_app.dart'; import 'package:devtools_app/src/shared/editor/api_classes.dart'; import 'package:devtools_app/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart'; +import 'package:devtools_app/src/standalone_ui/ide_shared/property_editor/property_editor_types.dart'; import 'package:devtools_app/src/standalone_ui/ide_shared/property_editor/property_editor_view.dart'; import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; @@ -61,7 +62,9 @@ void main() { final argsCompleter = Completer>(); listener = () { if (!argsCompleter.isCompleted) { - argsCompleter.complete(controller.editableWidgetData.value!.args); + argsCompleter.complete( + controller.editableWidgetData.value?.properties, + ); } }; controller.editableWidgetData.addListener(listener!); @@ -288,6 +291,95 @@ void main() { }); }); + group('filtering editable arguments', () { + testWidgets('can filter by name', (tester) async { + // Load the property editor. + await tester.pumpWidget(wrap(propertyEditor)); + + // Change the editable args. + controller.initForTestsOnly(editableArgsResult: result1); + await tester.pumpAndSettle(); + + final titleInput = _findTextFormField('String? title'); + final widthInput = _findTextFormField('double width'); + final heightInput = _findTextFormField('double? height'); + + // Verify all inputs are visible. + expect(_findNoPropertiesMessage, findsNothing); + expect(titleInput, findsOneWidget); + expect(widthInput, findsOneWidget); + expect(heightInput, findsOneWidget); + + // Filter by the "width" property. + final filterField = _findFilterField(); + expect(filterField, findsOneWidget); + await _inputText(filterField, text: 'width', tester: tester); + + // Verify only the "width" property is visible. + expect(widthInput, findsOneWidget); + expect(titleInput, findsNothing); + expect(heightInput, findsNothing); + }); + + testWidgets('can filter by type', (tester) async { + // Load the property editor. + await tester.pumpWidget(wrap(propertyEditor)); + + // Change the editable args. + controller.initForTestsOnly(editableArgsResult: result1); + await tester.pumpAndSettle(); + + final titleInput = _findTextFormField('String? title'); + final widthInput = _findTextFormField('double width'); + final heightInput = _findTextFormField('double? height'); + + // Verify all inputs are visible. + expect(_findNoPropertiesMessage, findsNothing); + expect(titleInput, findsOneWidget); + expect(widthInput, findsOneWidget); + expect(heightInput, findsOneWidget); + + // Filter by the "double" type. + final filterField = _findFilterField(); + expect(filterField, findsOneWidget); + await _inputText(filterField, text: 'double', tester: tester); + + // Verify only the "width" and "height" properties are visible. + expect(widthInput, findsOneWidget); + expect(heightInput, findsOneWidget); + expect(titleInput, findsNothing); + }); + + testWidgets('can filter by value', (tester) async { + // Load the property editor. + await tester.pumpWidget(wrap(propertyEditor)); + + // Change the editable args. + controller.initForTestsOnly(editableArgsResult: result1); + await tester.pumpAndSettle(); + + final titleInput = _findTextFormField('String? title'); + final widthInput = _findTextFormField('double width'); + final heightInput = _findTextFormField('double? height'); + + // Verify all inputs are visible. + expect(_findNoPropertiesMessage, findsNothing); + expect(titleInput, findsOneWidget); + expect(widthInput, findsOneWidget); + expect(heightInput, findsOneWidget); + + // Filter by the "Hello world!" value. + final filterField = _findFilterField(); + expect(filterField, findsOneWidget); + await _inputText(filterField, text: 'Hello world!', tester: tester); + + // Verify only the "title" property is visible. + expect(titleInput, findsOneWidget); + expect(widthInput, findsNothing); + expect(heightInput, findsNothing); + }); + }); + group('editing arguments', () { late Completer nextEditCompleter; @@ -713,6 +805,11 @@ final _findNoPropertiesMessage = find.text( 'No widget properties at current cursor location.', ); +Finder _findFilterField() => find.descendant( + of: find.byType(StandaloneFilterField), + matching: find.byType(TextField), +); + Finder _findTextFormField(String inputName) => find.ancestor( of: find.richTextContaining(inputName), matching: find.byType(TextFormField),