Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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<EditableArgument> args, String? name, String? documentation});
({List<EditableProperty> properties, String? name, String? documentation});

typedef EditArgumentFunction =
Future<EditArgumentResponse?> Function<T>({
Expand All @@ -21,7 +23,7 @@ typedef EditArgumentFunction =
});

class PropertyEditorController extends DisposableController
with AutoDisposeControllerMixin {
with AutoDisposeControllerMixin, FilterControllerMixin<EditableProperty> {
PropertyEditorController(this.editorClient) {
init();
}
Expand All @@ -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;
Expand Down Expand Up @@ -81,6 +84,27 @@ class PropertyEditorController extends DisposableController
super.dispose();
}

@override
void filterData(Filter<EditableProperty> filter) {
super.filterData(filter);
final filtered = (_editableWidgetData.value?.properties ?? []).where(
(property) => property.matchesSearchToken(
RegExp(filter.queryFilter.query, caseSensitive: false),
),
);
filteredData
..clear()
..addAll(filtered);
}

@override
void setActiveFilter({
String? query,
SettingFilters<EditableArgument>? settingFilters,
}) {
super.setActiveFilter(query: query);
Comment thread
elliette marked this conversation as resolved.
Outdated
}

Future<EditArgumentResponse?> editArgument<T>({
required String name,
required T value,
Expand All @@ -107,13 +131,18 @@ class PropertyEditorController extends DisposableController
textDocument: textDocument,
position: cursorPosition,
);
final args = result?.args ?? <EditableArgument>[];
final properties =
(result?.args ?? <EditableArgument>[])
.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,
Expand All @@ -127,9 +156,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,
);
Expand All @@ -140,5 +171,6 @@ class PropertyEditorController extends DisposableController
if (cursorPosition != null) {
_currentCursorPosition = cursorPosition;
}
filterData(activeFilter.value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import 'package:devtools_app_shared/utils.dart';
import 'package:meta/meta.dart';

import '../../../shared/editor/api_classes.dart';
import '../../../shared/primitives/utils.dart';
import '../../../shared/ui/search.dart';

/// Record representing an option for an [EditableProperty].
typedef PropertyOption = ({String text, bool isDefault});
Expand Down Expand Up @@ -130,7 +132,7 @@ class EditableEnum extends EditableProperty with FiniteValuesProperty {
}
}

class EditableProperty extends EditableArgument {
class EditableProperty extends EditableArgument with SearchableDataMixin {
Comment thread
elliette marked this conversation as resolved.
Outdated
EditableProperty(EditableArgument argument)
: super(
name: argument.name,
Expand Down Expand Up @@ -167,6 +169,13 @@ class EditableProperty extends EditableArgument {
Object? convertFromInputString(String? _) {
throw UnimplementedError();
}

@override
bool matchesSearchToken(RegExp regExpSearch) {
return name.caseInsensitiveContains(regExpSearch) ||
valueDisplay.caseInsensitiveContains(regExpSearch) ||
type.caseInsensitiveContains(regExpSearch);
}
}

mixin NumericProperty on EditableProperty {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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?;
Expand All @@ -42,7 +44,8 @@ class PropertyEditorView extends StatelessWidget {
);
}

final (:args, :name, :documentation) = editableWidgetData;
final filteredProperties = values.fourth as List<EditableProperty>;
final (:properties, :name, :documentation) = editableWidgetData;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expand All @@ -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,
),
],
);
Expand All @@ -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<EditableProperty> editableProperties;
final EditArgumentFunction editProperty;

static const defaultItemPadding = borderPadding;
static const denseItemPadding = defaultItemPadding / 2;
Expand Down Expand Up @@ -99,10 +102,13 @@ class _PropertiesListState extends State<_PropertiesList> {
Widget build(BuildContext context) {
return Column(
children: <Widget>[
_FilterControls(controller: widget.controller),
if (widget.editableProperties.isEmpty)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if editableProperties is empty and the filter query is empty? In this case, this message may not make sense

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is actually handled before we get here - if there are no editable properties at all we display a message instead of rendering the _PropertiesList:

const _NoMatchingPropertiesMessage(),
for (final property in widget.editableProperties)
_EditablePropertyItem(
property: property,
editProperty: widget.editProperty,
editProperty: widget.controller.editArgument,
),
].joinWith(const PaddedDivider.noPadding()),
);
Expand Down Expand Up @@ -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<EditableProperty>(
controller: controller,
filteredItem: 'property',
),
),
],
),
);
}
}

class _PropertyLabels extends StatelessWidget {
const _PropertyLabels({required this.property});

Expand Down Expand Up @@ -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});

Expand Down Expand Up @@ -320,7 +358,7 @@ class _WidgetNameAndDocumentation extends StatelessWidget {
),
],
),
const PaddedDivider(),
const PaddedDivider(padding: EdgeInsets.all(noPadding)),
Comment thread
elliette marked this conversation as resolved.
Outdated
],
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -61,7 +62,9 @@ void main() {
final argsCompleter = Completer<List<EditableArgument>>();
listener = () {
if (!argsCompleter.isCompleted) {
argsCompleter.complete(controller.editableWidgetData.value!.args);
argsCompleter.complete(
controller.editableWidgetData.value?.properties,
);
}
};
controller.editableWidgetData.addListener(listener!);
Expand Down Expand Up @@ -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<String> nextEditCompleter;

Expand Down Expand Up @@ -713,6 +805,11 @@ final _findNoPropertiesMessage = find.text(
'No widget properties at current cursor location.',
);

Finder _findFilterField() => find.descendant(
of: find.byType(StandaloneFilterField<EditableProperty>),
matching: find.byType(TextField),
);

Finder _findTextFormField(String inputName) => find.ancestor(
of: find.richTextContaining(inputName),
matching: find.byType(TextFormField),
Expand Down
Loading