Skip to content

Commit a77b33e

Browse files
authored
[Property Editor] Use the widget's starting position to create the key for the Property Editor (#9087)
1 parent 6d8ef36 commit a77b33e

4 files changed

Lines changed: 125 additions & 17 deletions

File tree

packages/devtools_app/lib/src/shared/editor/api_classes.dart

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ abstract class Field {
100100
static const documentation = 'documentation';
101101
static const emulator = 'emulator';
102102
static const emulatorId = 'emulatorId';
103+
static const end = 'end';
103104
static const ephemeral = 'ephemeral';
104105
static const errorText = 'errorText';
105106
static const flutterDeviceId = 'flutterDeviceId';
@@ -122,10 +123,12 @@ abstract class Field {
122123
static const platformType = 'platformType';
123124
static const prefersDebugSession = 'prefersDebugSession';
124125
static const projectRootPath = 'projectRootPath';
126+
static const range = 'range';
125127
static const requiresDebugSession = 'requiresDebugSession';
126128
static const result = 'result';
127129
static const selectedDeviceId = 'selectedDeviceId';
128130
static const selections = 'selections';
131+
static const start = 'start';
129132
static const supported = 'supported';
130133
static const supportsForceExternal = 'supportsForceExternal';
131134
static const textDocument = 'textDocument';
@@ -393,6 +396,31 @@ class EditorSelection with Serializable {
393396
};
394397
}
395398

399+
/// A range in the editor expressed as (zero-based) start and end positions.
400+
class EditorRange with Serializable {
401+
EditorRange({required this.start, required this.end});
402+
403+
EditorRange.fromJson(Map<String, Object?> map)
404+
: this(
405+
start: CursorPosition.fromJson(
406+
map[Field.start] as Map<String, Object?>,
407+
),
408+
end: CursorPosition.fromJson(map[Field.end] as Map<String, Object?>),
409+
);
410+
411+
/// The range's start position.
412+
final CursorPosition start;
413+
414+
/// The range's end position.
415+
final CursorPosition end;
416+
417+
@override
418+
Map<String, Object?> toJson() => {
419+
Field.start: start.toJson(),
420+
Field.end: end.toJson(),
421+
};
422+
}
423+
396424
/// Representation of a single cursor position in the editor.
397425
///
398426
/// The cursor position is after the given [character] of the [line].
@@ -430,12 +458,23 @@ class CursorPosition with Serializable {
430458

431459
/// The result of an `editableArguments` request.
432460
class EditableArgumentsResult with Serializable {
433-
EditableArgumentsResult({required this.args, this.name, this.documentation});
461+
EditableArgumentsResult({
462+
required this.args,
463+
this.name,
464+
this.documentation,
465+
this.range,
466+
});
434467

435468
EditableArgumentsResult.fromJson(Map<String, Object?> map)
436469
: this(
437470
name: map[Field.name] as String?,
438471
documentation: map[Field.documentation] as String?,
472+
range:
473+
(map[Field.range] as Map<String, Object?>?) == null
474+
? null
475+
: EditorRange.fromJson(
476+
map[Field.range] as Map<String, Object?>,
477+
),
439478
args:
440479
(map[Field.arguments] as List<Object?>? ?? <Object?>[])
441480
.cast<Map<String, Object?>>()
@@ -446,9 +485,15 @@ class EditableArgumentsResult with Serializable {
446485
final List<EditableArgument> args;
447486
final String? name;
448487
final String? documentation;
488+
final EditorRange? range;
449489

450490
@override
451-
Map<String, Object?> toJson() => {Field.arguments: args};
491+
Map<String, Object?> toJson() => {
492+
Field.arguments: args,
493+
Field.name: name,
494+
Field.documentation: documentation,
495+
Field.range: range,
496+
};
452497
}
453498

454499
/// Errors that the Analysis Server returns for failed argument edits.

packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ typedef EditableWidgetData =
2121
String? name,
2222
String? documentation,
2323
String? fileUri,
24+
EditorRange? range,
2425
});
2526

2627
typedef EditArgumentFunction =
@@ -51,6 +52,7 @@ class PropertyEditorController extends DisposableController
5152
String? get widgetName => _editableWidgetData.value?.name;
5253
String? get widgetDocumentation => _editableWidgetData.value?.documentation;
5354
String? get fileUri => _editableWidgetData.value?.fileUri;
55+
EditorRange? get widgetRange => _editableWidgetData.value?.range;
5456

5557
ValueListenable<bool> get shouldReconnect => _shouldReconnect;
5658
final _shouldReconnect = ValueNotifier<bool>(false);
@@ -100,6 +102,7 @@ class PropertyEditorController extends DisposableController
100102
properties: [],
101103
name: null,
102104
documentation: null,
105+
range: null,
103106
fileUri: textDocument.uriAsString,
104107
);
105108
return;
@@ -147,6 +150,30 @@ class PropertyEditorController extends DisposableController
147150
);
148151
}
149152

153+
int hashProperty(EditableProperty property) {
154+
final widgetData = editableWidgetData.value;
155+
if (widgetData == null) {
156+
return Object.hash(property.name, property.type);
157+
}
158+
final range = widgetRange;
159+
return range == null
160+
? Object.hash(
161+
property.name,
162+
property.type,
163+
property.value, // Include the property value.
164+
widgetName,
165+
fileUri,
166+
)
167+
: Object.hash(
168+
property.name,
169+
property.type,
170+
fileUri,
171+
widgetName,
172+
range.start.line, // Include the start position of the property.
173+
range.start.character,
174+
);
175+
}
176+
150177
Future<void> _updateWithEditableArgs({
151178
required TextDocument textDocument,
152179
required CursorPosition cursorPosition,
@@ -166,11 +193,14 @@ class PropertyEditorController extends DisposableController
166193
.where((property) => !property.isDeprecated || property.hasArgument)
167194
.toList();
168195
final name = result?.name;
196+
final range = result?.range;
197+
169198
_editableWidgetData.value = (
170199
properties: properties,
171200
name: name,
172201
documentation: result?.documentation,
173202
fileUri: _currentDocument?.uriAsString,
203+
range: range,
174204
);
175205
filterData(activeFilter.value);
176206
// Register impression.
@@ -195,6 +225,7 @@ class PropertyEditorController extends DisposableController
195225
EditableArgumentsResult? editableArgsResult,
196226
TextDocument? document,
197227
CursorPosition? cursorPosition,
228+
EditorRange? range,
198229
}) {
199230
setActiveFilter();
200231
if (editableArgsResult != null) {
@@ -204,6 +235,7 @@ class PropertyEditorController extends DisposableController
204235
name: editableArgsResult.name,
205236
documentation: editableArgsResult.documentation,
206237
fileUri: document?.uriAsString,
238+
range: range,
207239
);
208240
}
209241
if (document != null) {

packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_inputs.dart

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,13 +208,16 @@ class _TextInputState<T> extends State<_TextInput<T>>
208208

209209
late final FocusNode _focusNode;
210210

211+
late final TextEditingController _controller;
212+
211213
late String _currentValue;
212214

213215
@override
214216
void initState() {
215217
super.initState();
216218
_currentValue = widget.property.valueDisplay;
217219
_focusNode = FocusNode(debugLabel: 'text-input-${widget.property.name}');
220+
_controller = TextEditingController(text: widget.property.valueDisplay);
218221

219222
addAutoDisposeListener(_focusNode, () async {
220223
if (_focusNode.hasFocus) return;
@@ -223,12 +226,20 @@ class _TextInputState<T> extends State<_TextInput<T>>
223226
});
224227
}
225228

229+
@override
230+
void didUpdateWidget(_TextInput<T> oldWidget) {
231+
super.didUpdateWidget(oldWidget);
232+
if (oldWidget.property != widget.property) {
233+
_setValueAndMaintainSelection(widget.property.valueDisplay);
234+
}
235+
}
236+
226237
@override
227238
Widget build(BuildContext context) {
228239
final theme = Theme.of(context);
229240
return TextFormField(
230241
focusNode: _focusNode,
231-
initialValue: widget.property.valueDisplay,
242+
controller: _controller,
232243
enabled: widget.property.isEditable,
233244
autovalidateMode: AutovalidateMode.onUserInteraction,
234245
validator: (text) => inputValidator(text, property: widget.property),
@@ -259,6 +270,28 @@ class _TextInputState<T> extends State<_TextInput<T>>
259270
editPropertyCallback: widget.editProperty,
260271
);
261272
}
273+
274+
/// Sets the text field's value to [newValue].
275+
///
276+
/// Determines what the correct text selection should be based on the previous
277+
/// selection. Without this, the entire text field contents would be selected
278+
/// after editing a property. For details, see:
279+
/// https://github.com/flutter/flutter/issues/161596
280+
void _setValueAndMaintainSelection(String newValue) {
281+
final previousSelection = _controller.selection;
282+
// If the previous selection is in range of the new text, use it. Otherwise,
283+
// set the empty selection at the end of the string.
284+
final newSelection =
285+
(newValue.length < previousSelection.end ||
286+
newValue.length < previousSelection.start)
287+
? TextSelection.collapsed(offset: newValue.length)
288+
: previousSelection;
289+
// Set the new value in the controller with the new selection.
290+
_controller.value = TextEditingValue(
291+
text: newValue,
292+
selection: newSelection,
293+
);
294+
}
262295
}
263296

264297
mixin _PropertyInputMixin<T extends StatefulWidget, U> on State<T> {

packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_view.dart

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ class PropertyEditorView extends StatelessWidget {
5757
return [introSentence, const HowToUseMessage()];
5858
}
5959

60-
final (:properties, :name, :documentation, :fileUri) = editableWidgetData;
60+
final (:properties, :name, :documentation, :fileUri, :range) =
61+
editableWidgetData;
6162
if (fileUri != null && !fileUri.endsWith('.dart')) {
6263
return [const NoDartCodeMessage(), const HowToUseMessage()];
6364
}
@@ -123,8 +124,7 @@ class _PropertiesListState extends State<_PropertiesList> {
123124
for (final property in properties)
124125
_EditablePropertyItem(
125126
property: property,
126-
editProperty: widget.controller.editArgument,
127-
widgetDocumentation: widget.controller.widgetDocumentation,
127+
controller: widget.controller,
128128
),
129129
].joinWith(const PaddedDivider.noPadding()),
130130
);
@@ -136,13 +136,11 @@ class _PropertiesListState extends State<_PropertiesList> {
136136
class _EditablePropertyItem extends StatelessWidget {
137137
const _EditablePropertyItem({
138138
required this.property,
139-
required this.editProperty,
140-
required this.widgetDocumentation,
139+
required this.controller,
141140
});
142141

143142
final EditableProperty property;
144-
final EditArgumentFunction editProperty;
145-
final String? widgetDocumentation;
143+
final PropertyEditorController controller;
146144

147145
@override
148146
Widget build(BuildContext context) {
@@ -162,13 +160,13 @@ class _EditablePropertyItem extends StatelessWidget {
162160
),
163161
child: _InfoTooltip(
164162
property: property,
165-
widgetDocumentation: widgetDocumentation,
163+
widgetDocumentation: controller.widgetDocumentation,
166164
),
167165
),
168166
Expanded(
169167
child: _PropertyInput(
170168
property: property,
171-
editProperty: editProperty,
169+
controller: controller,
172170
),
173171
),
174172
],
@@ -354,16 +352,16 @@ class _InfoTooltip extends StatelessWidget {
354352
}
355353

356354
class _PropertyInput extends StatelessWidget {
357-
const _PropertyInput({required this.property, required this.editProperty});
355+
const _PropertyInput({required this.property, required this.controller});
358356

359357
final EditableProperty property;
360-
final EditArgumentFunction editProperty;
358+
final PropertyEditorController controller;
361359

362360
@override
363361
Widget build(BuildContext context) {
364-
final argType = property.type;
365-
final propertyKey = Key(property.hashCode.toString());
366-
switch (argType) {
362+
final editProperty = controller.editArgument;
363+
final propertyKey = Key(controller.hashProperty(property).toString());
364+
switch (property.type) {
367365
case boolType:
368366
return BooleanInput(
369367
key: propertyKey,

0 commit comments

Comments
 (0)