Skip to content

Commit

Permalink
Can send edit requests from the Property Editor (flutter#8632)
Browse files Browse the repository at this point in the history
  • Loading branch information
elliette authored Dec 16, 2024
1 parent c388dc6 commit 2704e5d
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ enum EditorMethod {
enum LspMethod {
editableArguments(
methodName: 'experimental/dart/textDocument/editableArguments',
);
),
editArgument(methodName: 'experimental/dart/textDocument/editArgument');

const LspMethod({required this.methodName});

Expand Down
24 changes: 24 additions & 0 deletions packages/devtools_app/lib/src/service/editor/editor_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import 'dart:async';

import 'package:devtools_app_shared/utils.dart';
import 'package:dtd/dtd.dart';
import 'package:logging/logging.dart';

import '../../shared/analytics/constants.dart';
import 'api_classes.dart';

final _log = Logger('editor_client');

/// A client wrapper that connects to an editor over DTD.
///
/// Changes made to the editor services/events should be considered carefully to
Expand Down Expand Up @@ -246,6 +249,27 @@ class EditorClient extends DisposableController
: null;
}

/// Requests that the Analysis Server makes a code edit for an argument.
Future<void> editArgument<T>({
required TextDocument textDocument,
required CursorPosition position,
required String name,
required T value,
}) async {
final response = await _callLspApi(
LspMethod.editArgument,
params: {
'type': 'Object', // This is required by DTD.
'textDocument': textDocument.toJson(),
'position': position.toJson(),
'edit': {'name': name, 'newValue': value},
},
);
// TODO(elliette): Handle response, currently the response from the Analysis
// Server is null.
_log.info('editArgument response: ${response.result}');
}

Future<DTDResponse> _call(
EditorMethod method, {
Map<String, Object?>? params,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@ class PropertyEditorController extends DisposableController
ValueListenable<List<EditableArgument>> get editableArgs => _editableArgs;
final _editableArgs = ValueNotifier<List<EditableArgument>>([]);

@visibleForTesting
void updateEditableArgs(List<EditableArgument> args) {
_editableArgs.value = args;
}

void _init() {
autoDisposeStreamSubscription(
editorClient.activeLocationChangedStream.listen((event) async {
Expand All @@ -45,8 +40,37 @@ class PropertyEditorController extends DisposableController
position: cursorPosition,
);
final args = result?.args ?? <EditableArgument>[];
updateEditableArgs(args);
_editableArgs.value = args;
}),
);
}

Future<void> editArgument<T>({required String name, required T value}) async {
final document = _currentDocument;
final position = _currentCursorPosition;
if (document == null || position == null) return;
await editorClient.editArgument(
textDocument: document,
position: position,
name: name,
value: value,
);
}

@visibleForTesting
void initForTestsOnly({
List<EditableArgument>? editableArgs,
TextDocument? document,
CursorPosition? cursorPosition,
}) {
if (editableArgs != null) {
_editableArgs.value = editableArgs;
}
if (document != null) {
_currentDocument = document;
}
if (cursorPosition != null) {
_currentCursorPosition = cursorPosition;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ class _PropertiesList extends StatelessWidget {
)
: Column(
children: <Widget>[
...args.map((arg) => _EditablePropertyItem(argument: arg)),
...args.map(
(arg) => _EditablePropertyItem(
argument: arg,
controller: controller,
),
),
].joinWith(const PaddedDivider.noPadding()),
);
},
Expand All @@ -59,9 +64,13 @@ class _PropertiesList extends StatelessWidget {
}

class _EditablePropertyItem extends StatelessWidget {
const _EditablePropertyItem({required this.argument});
const _EditablePropertyItem({
required this.argument,
required this.controller,
});

final EditableArgument argument;
final PropertyEditorController controller;

@override
Widget build(BuildContext context) {
Expand All @@ -72,7 +81,7 @@ class _EditablePropertyItem extends StatelessWidget {
flex: 3,
child: Padding(
padding: const EdgeInsets.all(_PropertiesList.itemPadding),
child: _PropertyInput(argument: argument),
child: _PropertyInput(argument: argument, controller: controller),
),
),
if (argument.isRequired || argument.isDefault) ...[
Expand Down Expand Up @@ -117,35 +126,45 @@ class _PropertyLabels extends StatelessWidget {
}
}

class _PropertyInput extends StatelessWidget {
const _PropertyInput({required this.argument});
class _PropertyInput extends StatefulWidget {
const _PropertyInput({required this.argument, required this.controller});

final EditableArgument argument;
final PropertyEditorController controller;

@override
State<_PropertyInput> createState() => _PropertyInputState();
}

class _PropertyInputState extends State<_PropertyInput> {
String get typeError => 'Please enter a ${widget.argument.type}.';

String currentValue = '';

@override
Widget build(BuildContext context) {
final decoration = InputDecoration(
helperText: '',
errorText: argument.errorText,
errorText: widget.argument.errorText,
isDense: true,
label: Text(argument.name),
label: Text(widget.argument.name),
border: const OutlineInputBorder(),
);

switch (argument.type) {
switch (widget.argument.type) {
case 'enum':
case 'bool':
final options =
argument.type == 'bool'
widget.argument.type == 'bool'
? ['true', 'false']
: (argument.options ?? <String>[]);
options.add(argument.valueDisplay);
if (argument.isNullable) {
: (widget.argument.options ?? <String>[]);
options.add(widget.argument.valueDisplay);
if (widget.argument.isNullable) {
options.add('null');
}

return DropdownButtonFormField(
value: argument.valueDisplay,
value: widget.argument.valueDisplay,
decoration: decoration,
items:
options.toSet().toList().map((option) {
Expand All @@ -156,46 +175,103 @@ class _PropertyInput extends StatelessWidget {
child: Text(option),
);
}).toList(),
onChanged: (_) {},
onChanged: (newValue) async {
await _editArgument(newValue);
},
);
case 'double':
case 'int':
case 'string':
return TextFormField(
initialValue: argument.valueDisplay,
enabled: argument.isEditable,
initialValue: widget.argument.valueDisplay,
enabled: widget.argument.isEditable,
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: _inputValidator,
inputFormatters: [FilteringTextInputFormatter.singleLineFormatter],
decoration: decoration,
style: Theme.of(context).fixedFontStyle,
// TODO(https://github.com/flutter/devtools/issues/8531) Handle onChanged.
onChanged: (_) {},
onChanged: (newValue) {
currentValue = newValue;
},
onEditingComplete: () async {
await _editArgument(currentValue);
},
onTapOutside: (_) async {
await _editArgument(currentValue);
},
);
default:
return Text(argument.valueDisplay);
return Text(widget.argument.valueDisplay);
}
}

String? _inputValidator(String? inputValue) {
final isDouble = argument.type == 'double';
final isInt = argument.type == 'int';
Future<void> _editArgument(String? valueAsString) async {
final argName = widget.argument.name;

// Only validate numeric types.
if (!isDouble && !isInt) {
return null;
// Can edit values to null.
if (widget.argument.isNullable && valueAsString == null ||
(valueAsString == '' && widget.argument.type != 'string')) {
await widget.controller.editArgument(name: argName, value: null);
return;
}

final validationMessage =
'Please enter ${isInt ? 'an integer' : 'a double'}.';
if (inputValue == null || inputValue == '') {
return validationMessage;
switch (widget.argument.type) {
case 'string':
case 'enum':
await widget.controller.editArgument(
name: argName,
value: valueAsString,
);
break;
case 'bool':
await widget.controller.editArgument(
name: argName,
value:
valueAsString == 'true' || valueAsString == 'false'
? valueAsString == 'true'
: valueAsString, // The boolean value might be an expression.
);
break;
case 'double':
final numValue = _toNumber(valueAsString);
if (numValue != null) {
await widget.controller.editArgument(
name: argName,
value: numValue as double,
);
}
break;
case 'int':
final numValue = _toNumber(valueAsString);
if (numValue != null) {
await widget.controller.editArgument(
name: argName,
value: numValue as int,
);
}
break;
}
final numValue =
isInt ? int.tryParse(inputValue) : double.tryParse(inputValue);
}

String? _inputValidator(String? inputValue) {
final numValue = _toNumber(inputValue);
if (numValue == null) {
return validationMessage;
return typeError;
}
return null;
}

Object? _toNumber(String? valueAsString) {
if (valueAsString == null || valueAsString == '') return null;

final isDouble = widget.argument.type == 'double';
final isInt = widget.argument.type == 'int';
// Only try to convert numeric types.
if (!isDouble && !isInt) {
return null;
}

return isInt ? int.tryParse(valueAsString) : double.tryParse(valueAsString);
}
}
Loading

0 comments on commit 2704e5d

Please sign in to comment.