Add support + tests for closing labels
Change-Id: I415a352010044d3b4aeb156d9d11dbbd94e86461
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/104782
Commit-Queue: Danny Tuppeny <dantup@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart b/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart
index bfa8c77..8f4be64 100644
--- a/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart
+++ b/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart
@@ -10,11 +10,12 @@
// ignore_for_file: deprecated_member_use_from_same_package
// ignore_for_file: unnecessary_brace_in_string_interps
// ignore_for_file: unused_import
+// ignore_for_file: unused_shown_name
import 'dart:core' hide deprecated;
import 'dart:core' as core show deprecated;
import 'dart:convert' show JsonEncoder;
-
+import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
import 'package:analysis_server/src/protocol/protocol_internal.dart'
show listEqual, mapEqual;
@@ -70,6 +71,62 @@
String toString() => jsonEncoder.convert(toJson());
}
+class ClosingLabel implements ToJsonable {
+ static const jsonHandler =
+ const LspJsonHandler(ClosingLabel.canParse, ClosingLabel.fromJson);
+
+ ClosingLabel(this.range, this.label) {
+ if (range == null) {
+ throw 'range is required but was not provided';
+ }
+ if (label == null) {
+ throw 'label is required but was not provided';
+ }
+ }
+ static ClosingLabel fromJson(Map<String, dynamic> json) {
+ final range = json['range'] != null ? Range.fromJson(json['range']) : null;
+ final label = json['label'];
+ return new ClosingLabel(range, label);
+ }
+
+ final String label;
+ final Range range;
+
+ Map<String, dynamic> toJson() {
+ Map<String, dynamic> __result = {};
+ __result['range'] = range ?? (throw 'range is required but was not set');
+ __result['label'] = label ?? (throw 'label is required but was not set');
+ return __result;
+ }
+
+ static bool canParse(Object obj) {
+ return obj is Map<String, dynamic> &&
+ obj.containsKey('range') &&
+ Range.canParse(obj['range']) &&
+ obj.containsKey('label') &&
+ obj['label'] is String;
+ }
+
+ @override
+ bool operator ==(other) {
+ if (other is ClosingLabel) {
+ return range == other.range && label == other.label && true;
+ }
+ return false;
+ }
+
+ @override
+ int get hashCode {
+ int hash = 0;
+ hash = JenkinsSmiHash.combine(hash, range.hashCode);
+ hash = JenkinsSmiHash.combine(hash, label.hashCode);
+ return JenkinsSmiHash.finish(hash);
+ }
+
+ @override
+ String toString() => jsonEncoder.convert(toJson());
+}
+
class CompletionItemResolutionInfo implements ToJsonable {
static const jsonHandler = const LspJsonHandler(
CompletionItemResolutionInfo.canParse,
@@ -199,3 +256,66 @@
@override
String toString() => jsonEncoder.convert(toJson());
}
+
+class PublishClosingLabelsParams implements ToJsonable {
+ static const jsonHandler = const LspJsonHandler(
+ PublishClosingLabelsParams.canParse, PublishClosingLabelsParams.fromJson);
+
+ PublishClosingLabelsParams(this.uri, this.labels) {
+ if (uri == null) {
+ throw 'uri is required but was not provided';
+ }
+ if (labels == null) {
+ throw 'labels is required but was not provided';
+ }
+ }
+ static PublishClosingLabelsParams fromJson(Map<String, dynamic> json) {
+ final uri = json['uri'];
+ final labels = json['labels']
+ ?.map((item) => item != null ? ClosingLabel.fromJson(item) : null)
+ ?.cast<ClosingLabel>()
+ ?.toList();
+ return new PublishClosingLabelsParams(uri, labels);
+ }
+
+ final List<ClosingLabel> labels;
+ final String uri;
+
+ Map<String, dynamic> toJson() {
+ Map<String, dynamic> __result = {};
+ __result['uri'] = uri ?? (throw 'uri is required but was not set');
+ __result['labels'] = labels ?? (throw 'labels is required but was not set');
+ return __result;
+ }
+
+ static bool canParse(Object obj) {
+ return obj is Map<String, dynamic> &&
+ obj.containsKey('uri') &&
+ obj['uri'] is String &&
+ obj.containsKey('labels') &&
+ (obj['labels'] is List &&
+ (obj['labels'].every((item) => ClosingLabel.canParse(item))));
+ }
+
+ @override
+ bool operator ==(other) {
+ if (other is PublishClosingLabelsParams) {
+ return uri == other.uri &&
+ listEqual(labels, other.labels,
+ (ClosingLabel a, ClosingLabel b) => a == b) &&
+ true;
+ }
+ return false;
+ }
+
+ @override
+ int get hashCode {
+ int hash = 0;
+ hash = JenkinsSmiHash.combine(hash, uri.hashCode);
+ hash = JenkinsSmiHash.combine(hash, labels.hashCode);
+ return JenkinsSmiHash.finish(hash);
+ }
+
+ @override
+ String toString() => jsonEncoder.convert(toJson());
+}
diff --git a/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart b/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart
index de7720a2..af4a262 100644
--- a/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart
+++ b/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart
@@ -10,6 +10,7 @@
// ignore_for_file: deprecated_member_use_from_same_package
// ignore_for_file: unnecessary_brace_in_string_interps
// ignore_for_file: unused_import
+// ignore_for_file: unused_shown_name
import 'dart:core' hide deprecated;
import 'dart:core' as core show deprecated;
diff --git a/pkg/analysis_server/lib/src/lsp/constants.dart b/pkg/analysis_server/lib/src/lsp/constants.dart
index 54b1805..5e0c09b 100644
--- a/pkg/analysis_server/lib/src/lsp/constants.dart
+++ b/pkg/analysis_server/lib/src/lsp/constants.dart
@@ -51,6 +51,8 @@
abstract class CustomMethods {
static const DiagnosticServer = const Method('dart/diagnosticServer');
+ static const PublishClosingLabels =
+ const Method('dart/textDocument/publishClosingLabels');
static const AnalyzerStatus = const Method(r'$/analyzerStatus');
}
diff --git a/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart b/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
index 58eaf26..2cfd0a9 100644
--- a/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
+++ b/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
@@ -13,6 +13,7 @@
import 'package:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/analysis_server_abstract.dart';
import 'package:analysis_server/src/collections.dart';
+import 'package:analysis_server/src/computer/computer_closingLabels.dart';
import 'package:analysis_server/src/context_manager.dart';
import 'package:analysis_server/src/domain_completion.dart'
show CompletionDomainHandler;
@@ -360,6 +361,17 @@
));
}
+ void publishClosingLabels(String path, List<ClosingLabel> labels) {
+ final params =
+ new PublishClosingLabelsParams(Uri.file(path).toString(), labels);
+ final message = new NotificationMessage(
+ CustomMethods.PublishClosingLabels,
+ params,
+ jsonRpcVersion,
+ );
+ sendNotification(message);
+ }
+
void publishDiagnostics(String path, List<Diagnostic> errors) {
final params =
new PublishDiagnosticsParams(Uri.file(path).toString(), errors);
@@ -465,6 +477,15 @@
addContextsToDeclarationsTracker();
}
+ /// Returns `true` if closing labels should be sent for [file] with the given
+ /// absolute path.
+ bool shouldSendClosingLabelsFor(String file) {
+ // Closing labels should only be sent for open (priority) files in the workspace.
+ return initializationOptions.closingLabels &&
+ priorityFiles.contains(file) &&
+ contextManager.isInAnalysisRoot(file);
+ }
+
/**
* Returns `true` if errors should be reported for [file] with the given
* absolute path.
@@ -531,13 +552,15 @@
class LspInitializationOptions {
final bool onlyAnalyzeProjectsWithOpenFiles;
final bool suggestFromUnimportedLibraries;
+ final bool closingLabels;
LspInitializationOptions(dynamic options)
: onlyAnalyzeProjectsWithOpenFiles = options != null &&
options['onlyAnalyzeProjectsWithOpenFiles'] == true,
// suggestFromUnimportedLibraries defaults to true, so must be
// explicitly passed as false to disable.
suggestFromUnimportedLibraries = options == null ||
- options['suggestFromUnimportedLibraries'] != false;
+ options['suggestFromUnimportedLibraries'] != false,
+ closingLabels = options != null && options['closingLabels'] == true;
}
class LspPerformance {
@@ -580,6 +603,17 @@
analysisServer.publishDiagnostics(result.path, serverErrors);
}
+ if (result.unit != null) {
+ if (analysisServer.shouldSendClosingLabelsFor(path)) {
+ final labels =
+ new DartUnitClosingLabelsComputer(result.lineInfo, result.unit)
+ .compute()
+ .map((l) => toClosingLabel(result.lineInfo, l))
+ .toList();
+
+ analysisServer.publishClosingLabels(result.path, labels);
+ }
+ }
});
analysisDriver.exceptions.listen((nd.ExceptionResult result) {
String message = 'Analysis failed: ${result.path}';
diff --git a/pkg/analysis_server/lib/src/lsp/mapping.dart b/pkg/analysis_server/lib/src/lsp/mapping.dart
index 5963258d..0c4ef07 100644
--- a/pkg/analysis_server/lib/src/lsp/mapping.dart
+++ b/pkg/analysis_server/lib/src/lsp/mapping.dart
@@ -528,6 +528,11 @@
return getKindPreferences().firstWhere(isSupported, orElse: () => null);
}
+lsp.ClosingLabel toClosingLabel(
+ server.LineInfo lineInfo, server.ClosingLabel label) =>
+ lsp.ClosingLabel(
+ toRange(lineInfo, label.offset, label.length), label.label);
+
lsp.CompletionItem toCompletionItem(
lsp.TextDocumentClientCapabilitiesCompletion completionCapabilities,
HashSet<lsp.CompletionItemKind> supportedCompletionItemKinds,
diff --git a/pkg/analysis_server/test/lsp/closing_labels_test.dart b/pkg/analysis_server/test/lsp/closing_labels_test.dart
new file mode 100644
index 0000000..0732276
--- /dev/null
+++ b/pkg/analysis_server/test/lsp/closing_labels_test.dart
@@ -0,0 +1,92 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'server_abstract.dart';
+
+main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(ClosingLabelsTest);
+ });
+}
+
+@reflectiveTest
+class ClosingLabelsTest extends AbstractLspAnalysisServerTest {
+ test_afterChange() async {
+ final initialContent = 'main() {}';
+ final updatedContent = '''
+Widget build(BuildContext context) {
+ return new Row( // Row 1:9
+ children: <Widget>[ // Widget[] 2:14
+ new Text('a'),
+ new Text('b'),
+ ], // /Widget[] 5:5
+ ); // /Row 6:3
+}
+''';
+ await initialize(initializationOptions: {'closingLabels': true});
+
+ final labelsUpdateBeforeChange = waitForClosingLabels(mainFileUri);
+ openFile(mainFileUri, initialContent);
+ final labelsBeforeChange = await labelsUpdateBeforeChange;
+
+ final labelsUpdateAfterChange = waitForClosingLabels(mainFileUri);
+ replaceFile(1, mainFileUri, updatedContent);
+ final labelsAfterChange = await labelsUpdateAfterChange;
+
+ expect(labelsBeforeChange, isEmpty);
+ expect(labelsAfterChange, hasLength(2));
+
+ final first = labelsAfterChange.first;
+ final second = labelsAfterChange.last;
+
+ expect(first.label, equals('Row'));
+ expect(first.range.start.line, equals(1));
+ expect(first.range.start.character, equals(9));
+ expect(first.range.end.line, equals(6));
+ expect(first.range.end.character, equals(3));
+
+ expect(second.label, equals('<Widget>[]'));
+ expect(second.range.start.line, equals(2));
+ expect(second.range.start.character, equals(14));
+ expect(second.range.end.line, equals(5));
+ expect(second.range.end.character, equals(5));
+ }
+
+ test_initial() async {
+ final content = '''
+Widget build(BuildContext context) {
+ return new Row( // Row 1:9
+ children: <Widget>[ // Widget[] 2:14
+ new Text('a'),
+ new Text('b'),
+ ], // /Widget[] 5:5
+ ); // /Row 6:3
+}
+''';
+ await initialize(initializationOptions: {'closingLabels': true});
+
+ final closingLabelsUpdate = waitForClosingLabels(mainFileUri);
+ openFile(mainFileUri, content);
+ final labels = await closingLabelsUpdate;
+
+ expect(labels, hasLength(2));
+ final first = labels.first;
+ final second = labels.last;
+
+ expect(first.label, equals('Row'));
+ expect(first.range.start.line, equals(1));
+ expect(first.range.start.character, equals(9));
+ expect(first.range.end.line, equals(6));
+ expect(first.range.end.character, equals(3));
+
+ expect(second.label, equals('<Widget>[]'));
+ expect(second.range.start.line, equals(2));
+ expect(second.range.start.character, equals(14));
+ expect(second.range.end.line, equals(5));
+ expect(second.range.end.character, equals(5));
+ }
+}
diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart
index d2e229f..647cc28 100644
--- a/pkg/analysis_server/test/lsp/server_abstract.dart
+++ b/pkg/analysis_server/test/lsp/server_abstract.dart
@@ -739,14 +739,8 @@
await serverToClient.firstWhere((message) {
if (message is NotificationMessage &&
message.method == Method.textDocument_publishDiagnostics) {
- // This helper method is used both in in-process tests where we'll get
- // the real type back, and out-of-process integration tests where
- // params is `Map<String, dynamic>` so for convenience just
- // handle either here.
- diagnosticParams = message.params is PublishDiagnosticsParams
- ? message.params
- : PublishDiagnosticsParams.fromJson(message.params);
-
+ diagnosticParams =
+ _convertParams(message, PublishDiagnosticsParams.fromJson);
return diagnosticParams.uri == uri.toString();
}
return false;
@@ -754,6 +748,31 @@
return diagnosticParams.diagnostics;
}
+ /// A helper to simplify processing of results for both in-process tests (where
+ /// we'll get the real type back), and out-of-process integration tests (where
+ /// params is `Map<String, dynamic>` and needs to be fromJson'd).
+ T _convertParams<T>(
+ IncomingMessage message,
+ T Function(Map<String, dynamic>) fromJson,
+ ) {
+ return message.params is T ? message.params : fromJson(message.params);
+ }
+
+ Future<List<ClosingLabel>> waitForClosingLabels(Uri uri) async {
+ PublishClosingLabelsParams closingLabelsParams;
+ await serverToClient.firstWhere((message) {
+ if (message is NotificationMessage &&
+ message.method == CustomMethods.PublishClosingLabels) {
+ closingLabelsParams =
+ _convertParams(message, PublishClosingLabelsParams.fromJson);
+
+ return closingLabelsParams.uri == uri.toString();
+ }
+ return false;
+ });
+ return closingLabelsParams.labels;
+ }
+
Future<AnalyzerStatusParams> waitForAnalysisStart() =>
waitForAnalysisStatus(true);
@@ -765,13 +784,7 @@
await serverToClient.firstWhere((message) {
if (message is NotificationMessage &&
message.method == CustomMethods.AnalyzerStatus) {
- // This helper method is used both in in-process tests where we'll get
- // the real type back, and out-of-process integration tests where
- // params is `Map<String, dynamic>` so for convenience just
- // handle either here.
- params = message.params is AnalyzerStatusParams
- ? message.params
- : AnalyzerStatusParams.fromJson(message.params);
+ params = _convertParams(message, AnalyzerStatusParams.fromJson);
return params.isAnalyzing == analyzing;
}
return false;
diff --git a/pkg/analysis_server/test/lsp/test_all.dart b/pkg/analysis_server/test/lsp/test_all.dart
index 7973bac..a05ca45 100644
--- a/pkg/analysis_server/test/lsp/test_all.dart
+++ b/pkg/analysis_server/test/lsp/test_all.dart
@@ -9,6 +9,7 @@
import 'cancel_request_test.dart' as cancel_request;
import 'change_workspace_folders_test.dart' as change_workspace_folders;
import 'code_actions_assists_test.dart' as code_actions_assists;
+import 'closing_labels_test.dart' as closing_labels;
import 'code_actions_fixes_test.dart' as code_actions_fixes;
import 'code_actions_source_test.dart' as code_actions_source;
import 'completion_test.dart' as completion;
@@ -35,6 +36,7 @@
analyzer_status.main();
cancel_request.main();
change_workspace_folders.main();
+ closing_labels.main();
code_actions_assists.main();
code_actions_fixes.main();
code_actions_source.main();
diff --git a/pkg/analysis_server/tool/lsp_spec/README.md b/pkg/analysis_server/tool/lsp_spec/README.md
index f44d5ea..0d0ac7e 100644
--- a/pkg/analysis_server/tool/lsp_spec/README.md
+++ b/pkg/analysis_server/tool/lsp_spec/README.md
@@ -20,6 +20,7 @@
- `onlyAnalyzeProjectsWithOpenFiles`: When set to `true`, analysis will only be performed for projects that have open files rather than the root workspace folder. Defaults to `false`.
- `suggestFromUnimportedLibraries`: When set to `false`, completion will not include synbols that are not already imported into the current file. Defaults to `true`, though the client must additionally support `workspace/applyEdit` for these completions to be included.
+- `closingLabels`: When set to `true`, `dart/textDocument/publishClosingLabels` notifications will be sent with information to render editor closing labels.
## Method Status
@@ -104,3 +105,10 @@
Params: `{ isAnalyzing: boolean }`
Notifies the client when analysis starts/completes.
+
+### dart/textDocument/publishClosingLabels Notification
+
+Direction: Server -> Client
+Params: `{ uri: string, abels: { label: string, range: Range }[] }`
+
+Notifies the client when closing label information is available (or updated) for a file.
diff --git a/pkg/analysis_server/tool/lsp_spec/codegen_dart.dart b/pkg/analysis_server/tool/lsp_spec/codegen_dart.dart
index 2967651..cbaec69 100644
--- a/pkg/analysis_server/tool/lsp_spec/codegen_dart.dart
+++ b/pkg/analysis_server/tool/lsp_spec/codegen_dart.dart
@@ -34,8 +34,7 @@
name != 'ResourceOperationKind';
}
-String generateDartForTypes(List<AstNode> types) {
- // Keep maps of items we may need to look up quickly later.
+void recordTypes(List<AstNode> types) {
types
.whereType<TypeAlias>()
.forEach((alias) => _typeAliases[alias.name] = alias);
@@ -51,6 +50,9 @@
types
.whereType<Namespace>()
.forEach((namespace) => _namespaces[namespace.name] = namespace);
+}
+
+String generateDartForTypes(List<AstNode> types) {
final buffer = new IndentableStringBuffer();
_getSorted(types).forEach((t) => _writeType(buffer, t));
final formattedCode = _formatCode(buffer.toString());
diff --git a/pkg/analysis_server/tool/lsp_spec/generate_all.dart b/pkg/analysis_server/tool/lsp_spec/generate_all.dart
index 00cd091..70d21840 100644
--- a/pkg/analysis_server/tool/lsp_spec/generate_all.dart
+++ b/pkg/analysis_server/tool/lsp_spec/generate_all.dart
@@ -6,9 +6,8 @@
import 'dart:io';
import 'package:analysis_server/src/services/correction/strings.dart';
-import 'package:http/http.dart' as http;
-
import 'package:args/args.dart';
+import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
import 'codegen_dart.dart';
@@ -40,11 +39,27 @@
final String outFolder = path.join(packageFolder, 'lib', 'lsp_protocol');
new Directory(outFolder).createSync();
- await writeCustomClasses(args, outFolder);
- await writeSpecClasses(args, outFolder);
+ // Collect definitions for types in the spec and our custom extensions.
+ final specTypes = await getSpecClasses(args);
+ final customTypes = getCustomClasses();
+
+ // Record both sets of types in dictionaries for faster lookups, but also so
+ // they can reference each other and we can find the definitions during
+ // codegen.
+ recordTypes(specTypes);
+ recordTypes(customTypes);
+
+ // Generate formatted Dart code (as a string) for each set of types.
+ final String specTypesOutput = generateDartForTypes(specTypes);
+ final String customTypesOutput = generateDartForTypes(customTypes);
+
+ new File(path.join(outFolder, 'protocol_generated.dart')).writeAsStringSync(
+ generatedFileHeader(2018, importCustom: true) + specTypesOutput);
+ new File(path.join(outFolder, 'protocol_custom_generated.dart'))
+ .writeAsStringSync(generatedFileHeader(2019) + customTypesOutput);
}
-Future writeSpecClasses(ArgResults args, String outFolder) async {
+Future<List<AstNode>> getSpecClasses(ArgResults args) async {
if (args[argDownload]) {
await downloadSpec();
}
@@ -63,27 +78,32 @@
// Extract additional inline types that are specificed online in the `results`
// section of the doc.
types.addAll(extractResultsInlineTypes(spec));
-
- final String output = generateDartForTypes(types);
-
- new File(path.join(outFolder, 'protocol_generated.dart')).writeAsStringSync(
- generatedFileHeader(2018, importCustom: true) + output);
+ return types;
}
-/// Writes classes used by Dart's custom LSP methods.
-Future writeCustomClasses(ArgResults args, String outFolder) async {
+List<AstNode> getCustomClasses() {
interface(String name, List<Member> fields) {
return new Interface(null, Token.identifier(name), [], [], fields);
}
- field(String name, {String type, canBeNull: false, canBeUndefined: false}) {
- return new Field(null, Token.identifier(name), Type.identifier(type),
- canBeNull, canBeUndefined);
+ field(String name,
+ {String type, array: false, canBeNull: false, canBeUndefined: false}) {
+ var fieldType =
+ array ? ArrayType(Type.identifier(type)) : Type.identifier(type);
+
+ return new Field(
+ null, Token.identifier(name), fieldType, canBeNull, canBeUndefined);
}
final List<AstNode> customTypes = [
interface('DartDiagnosticServer', [field('port', type: 'number')]),
interface('AnalyzerStatusParams', [field('isAnalyzing', type: 'boolean')]),
+ interface('PublishClosingLabelsParams', [
+ field('uri', type: 'string'),
+ field('labels', type: 'ClosingLabel', array: true)
+ ]),
+ interface('ClosingLabel',
+ [field('range', type: 'Range'), field('label', type: 'string')]),
interface(
'CompletionItemResolutionInfo',
[
@@ -94,11 +114,7 @@
],
),
];
-
- final String output = generateDartForTypes(customTypes);
-
- new File(path.join(outFolder, 'protocol_custom_generated.dart'))
- .writeAsStringSync(generatedFileHeader(2019) + output);
+ return customTypes;
}
Namespace extractMethodsEnum(String spec) {
@@ -140,11 +156,12 @@
// ignore_for_file: deprecated_member_use_from_same_package
// ignore_for_file: unnecessary_brace_in_string_interps
// ignore_for_file: unused_import
+// ignore_for_file: unused_shown_name
import 'dart:core' hide deprecated;
import 'dart:core' as core show deprecated;
import 'dart:convert' show JsonEncoder;
-${importCustom ? "import 'package:analysis_server/lsp_protocol/protocol_custom_generated.dart';" : ''}
+import 'package:analysis_server/lsp_protocol/protocol${importCustom ? '_custom' : ''}_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
import 'package:analysis_server/src/protocol/protocol_internal.dart'
show listEqual, mapEqual;