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;