Skip to content

[Property Editor] Add APIs to support refactors from the Property Editor #9202

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
May 27, 2025
Merged
2 changes: 1 addition & 1 deletion flutter-candidate.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
48f87a5fe76aa65f89f37cc716e50b34933e78e9
dd671fae53d37eb15e0f8fc94cd52c2f2ff147ee
11 changes: 10 additions & 1 deletion packages/devtools_app/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,20 @@
"request": "attach",
},
{
"name": "standalone_ui/property_editor_sidebar",
"name": "property_editor_sidebar",
"request": "launch",
"type": "dart",
"program": "test/test_infra/scenes/standalone_ui/property_editor_sidebar.stager_app.g.dart",
},
{
"name": "property_editor_sidebar + experiments",
"request": "launch",
"type": "dart",
"program": "test/test_infra/scenes/standalone_ui/property_editor_sidebar.stager_app.g.dart",
"args": [
"--dart-define=enable_experiments=true"
]
},

]
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,6 @@ extension PropertyEditorSidebar on Never {
/// Analytics id to track events that come from the DTD editor sidebar.
static String get id => 'propertyEditorSidebar';

/// Identifier for errors returned from the getEditableArguments API.
static String get getEditableArgumentsIdentifier =>
'${id}Error-getEditableArguments';

/// Identifier for errors returned from the editArgument API.
static String get editArgumentIdentifier => '${id}Error-editArgument';

/// Analytics id for opening the documentation.
static String get documentationLink => 'propertyEditorDocumentation';

Expand Down
145 changes: 136 additions & 9 deletions packages/devtools_app/lib/src/shared/editor/api_classes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,36 @@ enum EditorMethod {
/// TODO(https://github.com/flutter/devtools/issues/8824): Add tests that these
/// are in-sync with analysis_server.
enum LspMethod {
codeAction(methodName: 'textDocument/codeAction'),
editableArguments(methodName: 'dart/textDocument/editableArguments'),
editArgument(methodName: 'dart/textDocument/editArgument');
editArgument(methodName: 'dart/textDocument/editArgument'),
executeCommand(methodName: 'workspace/executeCommand');

const LspMethod({required this.methodName});

/// Returns the [LspMethod] for the given [methodName].
///
/// If the [methodName] does not exist, returns null.
static LspMethod? fromMethodName(String methodName) {
for (final method in LspMethod.values) {
if (method.methodName == methodName) return method;
}
return null;
Copy link
Member

Choose a reason for hiding this comment

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

can this be return LspMethod.values.firstWhereOrNull((method) => method.methodName == methodName)

Copy link
Contributor

Choose a reason for hiding this comment

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

For a minor perf increase I think you could also add something like:

static nameMap = LspMethod.values.asNameMap();

And then look it up as:

return nameMap[methodName];`

Copy link
Member Author

Choose a reason for hiding this comment

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

Done! Though we need the method name map instead of the name map, so I've added a static map for method name -> method to use for the look-up.

}

final String methodName;

String get experimentalMethodName => 'experimental/$methodName';
static final _registrationStatus = <LspMethod, bool>{
for (final method in LspMethod.values) method: false,
};

/// Sets the registration status for this LSP method.
set isRegistered(bool isRegistered) {
_registrationStatus[this] = isRegistered;
}

/// Gets the current registration status of this LSP method.
bool get isRegistered => _registrationStatus[this] ?? false;
}

/// Known kinds of events that may come from the editor.
Expand Down Expand Up @@ -82,12 +104,14 @@ enum EditorEventKind {
/// Constants for all fields used in JSON maps to avoid literal strings that
/// may have typos sprinkled throughout the API classes.
abstract class Field {
static const actions = 'actions';
static const active = 'active';
static const anchor = 'anchor';
static const arguments = 'arguments';
static const backgroundColor = 'backgroundColor';
static const category = 'category';
static const character = 'character';
static const command = 'command';
static const debuggerType = 'debuggerType';
static const debugSession = 'debugSession';
static const debugSessionId = 'debugSessionId';
Expand Down Expand Up @@ -115,7 +139,9 @@ abstract class Field {
static const isEditable = 'isEditable';
static const isNullable = 'isNullable';
static const isRequired = 'isRequired';
static const kind = 'kind';
static const line = 'line';
static const loggedAction = 'loggedAction';
static const name = 'name';
static const options = 'options';
static const page = 'page';
Expand All @@ -133,6 +159,7 @@ abstract class Field {
static const supportsForceExternal = 'supportsForceExternal';
static const textDocument = 'textDocument';
static const theme = 'theme';
static const title = 'title';
static const type = 'type';
static const uri = 'uri';
static const value = 'value';
Expand Down Expand Up @@ -501,6 +528,105 @@ class EditableArgumentsResult with Serializable {
};
}

/// Constants for [CodeAction] prefixes used to filter the results returned by
/// an [LspMethod.codeAction] request.
abstract class CodeActionPrefixes {
Copy link
Member

Choose a reason for hiding this comment

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

can we add unit tests for all of the new logic in this file?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, added editor_client_test (also added test cases for editArgument and editableArguments)

static const flutterWrap = 'refactor.flutter.wrap';
}

/// The result of an [LspMethod.codeAction] request to the Analysis Server.
///
/// Contains a list of [CodeAction]s that can be performed.
class CodeActionResult with Serializable {
CodeActionResult({required this.actions});

CodeActionResult.fromJson(List<Map<String, Object?>> list)
: this(actions: list.map(CodeAction.fromJson).toList());

final List<CodeAction> actions;

@override
Map<String, Object?> toJson() => {Field.actions: actions};
}

/// A code action (also known as a "Refactor" or "Quick Fix") that can be called
/// via an [LspMethod.executeCommand] request.
///
/// For example, "Wrap with Center" or "Wrap with Container".
class CodeAction with Serializable {
CodeAction({required this.command, required this.title, required this.args});

CodeAction.fromJson(Map<String, Object?> map)
: this(
command: map[Field.command] as String,
title: map[Field.title] as String,
args: (map[Field.arguments] as List<Object?>? ?? <Object?>[])
.cast<Map<String, Object?>>()
.map(CodeActionArgument.fromJson)
.toList(),
);

/// The command identifier to send to [LspMethod.executeCommand].
final String? command;

/// The human-readable title of the command, e.g., "Wrap with Center".
final String? title;

/// Arguments that should be passed to [LspMethod.executeCommand] when
/// invoking this action.
final List<CodeActionArgument> args;

@override
Map<String, Object?> toJson() => {
Field.command: command,
Field.title: title,
Field.arguments: args,
};
}

/// An argument for a [CodeAction].
///
/// This includes information about the document and range to which the
/// [CodeAction] should be applied.
class CodeActionArgument with Serializable {
CodeActionArgument({
required this.textDocument,
required this.range,
required this.kind,
required this.loggedAction,
});

CodeActionArgument.fromJson(Map<String, Object?> map)
: this(
textDocument: TextDocument.fromJson(
map[Field.textDocument] as Map<String, Object?>,
),
range: EditorRange.fromJson(map[Field.range] as Map<String, Object?>),
kind: map[Field.kind] as String?,
loggedAction: map[Field.loggedAction] as String?,
);

/// The document to which the [CodeAction] applies.
final TextDocument textDocument;

/// The range within the [textDocument] to which the [CodeAction] applies.
final EditorRange range;

/// The kind of action, often a string like "refactor.flutter.wrap.container".
final String? kind;

/// An identifier used for logging or analytics purposes related to this action.
final String? loggedAction;

@override
Map<String, Object?> toJson() => {
Field.textDocument: textDocument.toJson(),
Field.range: range.toJson(),
Field.kind: kind,
Field.loggedAction: loggedAction,
};
}

/// Errors that the Analysis Server returns for failed argument edits.
///
/// These should be kept in sync with the error coes defined at
Expand Down Expand Up @@ -554,16 +680,17 @@ enum EditArgumentError {
}
}

/// Response to an edit argument request.
class EditArgumentResponse {
EditArgumentResponse({required this.success, this.errorMessage, errorCode})
: _errorCode = errorCode;
/// Generic response representing whether a reqeust was a [success].
class GenericApiResponse {
GenericApiResponse({
required this.success,
this.errorMessage,
this.errorCode,
});

final bool success;
final String? errorMessage;
final int? _errorCode;

EditArgumentError? get errorType => EditArgumentError.fromCode(_errorCode);
final int? errorCode;
}

/// Information about a single editable argument of a widget.
Expand Down
Loading