generate dartdocs for grpc services (#993)

generate dartdocs for grpc services; fix https://github.com/google/protobuf.dart/issues/973
diff --git a/protoc_plugin/CHANGELOG.md b/protoc_plugin/CHANGELOG.md
index c41f2f0..5c5fe95 100644
--- a/protoc_plugin/CHANGELOG.md
+++ b/protoc_plugin/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 22.1.1-wip
+
+* Generate dartdocs for grpc services ([#973]).
+
+[#973]: https://github.com/google/protobuf.dart/issues/973
+
 ## 22.1.0
 
 * Fix factory argument types for protobuf `Map` fields. ([#975])
diff --git a/protoc_plugin/lib/src/client_generator.dart b/protoc_plugin/lib/src/client_generator.dart
index cc82735..7112e06 100644
--- a/protoc_plugin/lib/src/client_generator.dart
+++ b/protoc_plugin/lib/src/client_generator.dart
@@ -10,26 +10,28 @@
   final String className;
   final Set<String> usedMethodNames = {...reservedMemberNames};
 
-  /// Tag of `FileDescriptorProto.service`.
-  static const _fileDescriptorServiceTag = 6;
-
   /// Tag of `ServiceDescriptorProto.method`.
-  static const _serviceDescriptorMethodTag = 2;
+  static const serviceDescriptorMethodTag = 2;
+
+  /// Tag of `FileDescriptorProto.service`.
+  static const fileDescriptorServiceTag = 6;
 
   /// Index of the service in `FileDescriptorProto.service` repeated field.
   final int _repeatedFieldIndex;
 
-  List<int> get _serviceDescriptorPath => [
-        ...service.fileGen.fieldPath,
-        _fileDescriptorServiceTag,
-        _repeatedFieldIndex
-      ];
+  late final List<int> _serviceDescriptorPath = [
+    ...service.fileGen.fieldPath,
+    fileDescriptorServiceTag,
+    _repeatedFieldIndex
+  ];
 
-  List<int> _methodDescriptorPath(int methodRepeatedFieldIndex) => [
-        ..._serviceDescriptorPath,
-        _serviceDescriptorMethodTag,
-        methodRepeatedFieldIndex
-      ];
+  List<int> _methodDescriptorPath(int methodIndex) {
+    return [
+      ..._serviceDescriptorPath,
+      serviceDescriptorMethodTag,
+      methodIndex,
+    ];
+  }
 
   ClientApiGenerator(
       this.service, Set<String> usedNames, this._repeatedFieldIndex)
@@ -63,21 +65,22 @@
   }
 
   // Subclasses can override this.
-  void generateMethod(IndentingWriter out, MethodDescriptorProto m,
-      int methodRepeatedFieldIndex) {
+  void generateMethod(
+      IndentingWriter out, MethodDescriptorProto method, int methodIndex) {
     final methodName = disambiguateName(
-        avoidInitialUnderscore(service._methodName(m.name)),
+        avoidInitialUnderscore(service._methodName(method.name)),
         usedMethodNames,
         defaultSuffixes());
-    final inputType = service._getDartClassName(m.inputType, forMainFile: true);
+    final inputType =
+        service._getDartClassName(method.inputType, forMainFile: true);
     final outputType =
-        service._getDartClassName(m.outputType, forMainFile: true);
-    final commentBlock = service.fileGen
-        .commentBlock(_methodDescriptorPath(methodRepeatedFieldIndex));
+        service._getDartClassName(method.outputType, forMainFile: true);
+    final commentBlock =
+        service.fileGen.commentBlock(_methodDescriptorPath(methodIndex));
     if (commentBlock != null) {
       out.println(commentBlock);
     }
-    if (m.options.deprecated) {
+    if (method.options.deprecated) {
       out.println(
           '@$coreImportPrefix.Deprecated(\'This method is deprecated\')');
     }
@@ -86,7 +89,7 @@
             '$protobufImportPrefix.ClientContext? ctx, $inputType request) =>',
         ';', () {
       out.println('_client.invoke<$outputType>(ctx, \'$className\', '
-          '\'${m.name}\', request, $outputType())');
+          '\'${method.name}\', request, $outputType())');
     });
   }
 }
diff --git a/protoc_plugin/lib/src/code_generator.dart b/protoc_plugin/lib/src/code_generator.dart
index 1afec45..dd4c19f 100644
--- a/protoc_plugin/lib/src/code_generator.dart
+++ b/protoc_plugin/lib/src/code_generator.dart
@@ -32,7 +32,7 @@
   /// the message in question.
   /// For more information see
   /// https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/descriptor.proto#L728
-  List<int>? get fieldPath;
+  List<int> get fieldPath;
 
   /// The fully qualified name with a leading '.'.
   ///
diff --git a/protoc_plugin/lib/src/enum_generator.dart b/protoc_plugin/lib/src/enum_generator.dart
index 287a4fe..ea26f34 100644
--- a/protoc_plugin/lib/src/enum_generator.dart
+++ b/protoc_plugin/lib/src/enum_generator.dart
@@ -29,12 +29,10 @@
   /// Maps the name of an enum value to the Dart name we will use for it.
   final Map<String, String> dartNames = <String, String>{};
   final List<int> _originalAliasIndices = <int>[];
-  List<int>? _fieldPath;
   final List<int> _fieldPathSegment;
 
   @override
-  List<int> get fieldPath =>
-      _fieldPath ??= List.from(parent.fieldPath!)..addAll(_fieldPathSegment);
+  late final List<int> fieldPath = [...parent.fieldPath, ..._fieldPathSegment];
 
   EnumGenerator._(EnumDescriptorProto descriptor, this.parent,
       Set<String> usedClassNames, int repeatedFieldIndex, int fieldIdTag)
diff --git a/protoc_plugin/lib/src/extension_generator.dart b/protoc_plugin/lib/src/extension_generator.dart
index f9eef30..03ace98 100644
--- a/protoc_plugin/lib/src/extension_generator.dart
+++ b/protoc_plugin/lib/src/extension_generator.dart
@@ -16,8 +16,7 @@
   final List<int> _fieldPathSegment;
 
   /// See [ProtobufContainer]
-  late final List<int> fieldPath = List.from(_parent.fieldPath!)
-    ..addAll(_fieldPathSegment);
+  late final List<int> fieldPath = [..._parent.fieldPath, ..._fieldPathSegment];
 
   ExtensionGenerator._(this._descriptor, this._parent, Set<String> usedNames,
       int repeatedFieldIndex, int fieldIdTag)
diff --git a/protoc_plugin/lib/src/file_generator.dart b/protoc_plugin/lib/src/file_generator.dart
index 0eb7a35..cb06ca0 100644
--- a/protoc_plugin/lib/src/file_generator.dart
+++ b/protoc_plugin/lib/src/file_generator.dart
@@ -175,7 +175,7 @@
     for (var i = 0; i < descriptor.service.length; i++) {
       final service = descriptor.service[i];
       if (options.useGrpc) {
-        grpcGenerators.add(GrpcServiceGenerator(service, this));
+        grpcGenerators.add(GrpcServiceGenerator(service, this, i));
       } else {
         final serviceGen =
             ServiceGenerator(service, this, usedTopLevelServiceNames);
diff --git a/protoc_plugin/lib/src/generated/dart_options.pb.dart b/protoc_plugin/lib/src/generated/dart_options.pb.dart
index 340247f..6de543c 100644
--- a/protoc_plugin/lib/src/generated/dart_options.pb.dart
+++ b/protoc_plugin/lib/src/generated/dart_options.pb.dart
@@ -164,14 +164,14 @@
       _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Imports>(create);
   static Imports? _defaultInstance;
 
-  ///  Mixins to be used on messages in this file.
-  ///  These mixins are in addition to internally defined mixins (e.g PbMapMixin)
-  ///  and may override them.
+  /// Mixins to be used on messages in this file.
+  /// These mixins are in addition to internally defined mixins (e.g PbMapMixin)
+  /// and may override them.
   ///
-  ///  Warning: mixins are experimental. The protoc Dart plugin doesn't check
-  ///  for name conflicts between mixin class members and generated class members,
-  ///  so the generated code may contain errors. Therefore, running dartanalyzer
-  ///  on the generated file is a good idea.
+  /// Warning: mixins are experimental. The protoc Dart plugin doesn't check
+  /// for name conflicts between mixin class members and generated class members,
+  /// so the generated code may contain errors. Therefore, running dartanalyzer
+  /// on the generated file is a good idea.
   @$pb.TagNumber(1)
   $pb.PbList<DartMixin> get mixins => $_getList(0);
 }
diff --git a/protoc_plugin/lib/src/generated/descriptor.pb.dart b/protoc_plugin/lib/src/generated/descriptor.pb.dart
index a4dfd4c..b505a40 100644
--- a/protoc_plugin/lib/src/generated/descriptor.pb.dart
+++ b/protoc_plugin/lib/src/generated/descriptor.pb.dart
@@ -948,27 +948,27 @@
   @$pb.TagNumber(10)
   void clearJsonName() => $_clearField(10);
 
-  ///  If true, this is a proto3 "optional". When a proto3 field is optional, it
-  ///  tracks presence regardless of field type.
+  /// If true, this is a proto3 "optional". When a proto3 field is optional, it
+  /// tracks presence regardless of field type.
   ///
-  ///  When proto3_optional is true, this field must be belong to a oneof to
-  ///  signal to old proto3 clients that presence is tracked for this field. This
-  ///  oneof is known as a "synthetic" oneof, and this field must be its sole
-  ///  member (each proto3 optional field gets its own synthetic oneof). Synthetic
-  ///  oneofs exist in the descriptor only, and do not generate any API. Synthetic
-  ///  oneofs must be ordered after all "real" oneofs.
+  /// When proto3_optional is true, this field must be belong to a oneof to
+  /// signal to old proto3 clients that presence is tracked for this field. This
+  /// oneof is known as a "synthetic" oneof, and this field must be its sole
+  /// member (each proto3 optional field gets its own synthetic oneof). Synthetic
+  /// oneofs exist in the descriptor only, and do not generate any API. Synthetic
+  /// oneofs must be ordered after all "real" oneofs.
   ///
-  ///  For message fields, proto3_optional doesn't create any semantic change,
-  ///  since non-repeated message fields always track presence. However it still
-  ///  indicates the semantic detail of whether the user wrote "optional" or not.
-  ///  This can be useful for round-tripping the .proto file. For consistency we
-  ///  give message fields a synthetic oneof also, even though it is not required
-  ///  to track presence. This is especially important because the parser can't
-  ///  tell if a field is a message or an enum, so it must always create a
-  ///  synthetic oneof.
+  /// For message fields, proto3_optional doesn't create any semantic change,
+  /// since non-repeated message fields always track presence. However it still
+  /// indicates the semantic detail of whether the user wrote "optional" or not.
+  /// This can be useful for round-tripping the .proto file. For consistency we
+  /// give message fields a synthetic oneof also, even though it is not required
+  /// to track presence. This is especially important because the parser can't
+  /// tell if a field is a message or an enum, so it must always create a
+  /// synthetic oneof.
   ///
-  ///  Proto2 optional fields do not set this flag, because they already indicate
-  ///  optional with `LABEL_OPTIONAL`.
+  /// Proto2 optional fields do not set this flag, because they already indicate
+  /// optional with `LABEL_OPTIONAL`.
   @$pb.TagNumber(17)
   $core.bool get proto3Optional => $_getBF(10);
   @$pb.TagNumber(17)
@@ -1065,12 +1065,12 @@
   OneofOptions ensureOptions() => $_ensure(1);
 }
 
-///  Range of reserved numeric values. Reserved values may not be used by
-///  entries in the same enum. Reserved ranges may not overlap.
+/// Range of reserved numeric values. Reserved values may not be used by
+/// entries in the same enum. Reserved ranges may not overlap.
 ///
-///  Note that this is distinct from DescriptorProto.ReservedRange in that it
-///  is inclusive such that it can appropriately represent the entire int32
-///  domain.
+/// Note that this is distinct from DescriptorProto.ReservedRange in that it
+/// is inclusive such that it can appropriately represent the entire int32
+/// domain.
 class EnumDescriptorProto_EnumReservedRange extends $pb.GeneratedMessage {
   factory EnumDescriptorProto_EnumReservedRange({
     $core.int? start,
@@ -1861,16 +1861,16 @@
   @$pb.TagNumber(11)
   void clearGoPackage() => $_clearField(11);
 
-  ///  Should generic services be generated in each language?  "Generic" services
-  ///  are not specific to any particular RPC system.  They are generated by the
-  ///  main code generators in each language (without additional plugins).
-  ///  Generic services were the only kind of service generation supported by
-  ///  early versions of google.protobuf.
+  /// Should generic services be generated in each language?  "Generic" services
+  /// are not specific to any particular RPC system.  They are generated by the
+  /// main code generators in each language (without additional plugins).
+  /// Generic services were the only kind of service generation supported by
+  /// early versions of google.protobuf.
   ///
-  ///  Generic services are now considered deprecated in favor of using plugins
-  ///  that generate code specific to your particular RPC system.  Therefore,
-  ///  these default to false.  Old code which depends on generic services should
-  ///  explicitly set them to true.
+  /// Generic services are now considered deprecated in favor of using plugins
+  /// that generate code specific to your particular RPC system.  Therefore,
+  /// these default to false.  Old code which depends on generic services should
+  /// explicitly set them to true.
   @$pb.TagNumber(16)
   $core.bool get ccGenericServices => $_getBF(5);
   @$pb.TagNumber(16)
@@ -2163,24 +2163,24 @@
       $pb.GeneratedMessage.$_defaultFor<MessageOptions>(create);
   static MessageOptions? _defaultInstance;
 
-  ///  Set true to use the old proto1 MessageSet wire format for extensions.
-  ///  This is provided for backwards-compatibility with the MessageSet wire
-  ///  format.  You should not use this for any other reason:  It's less
-  ///  efficient, has fewer features, and is more complicated.
+  /// Set true to use the old proto1 MessageSet wire format for extensions.
+  /// This is provided for backwards-compatibility with the MessageSet wire
+  /// format.  You should not use this for any other reason:  It's less
+  /// efficient, has fewer features, and is more complicated.
   ///
-  ///  The message must be defined exactly as follows:
-  ///    message Foo {
-  ///      option message_set_wire_format = true;
-  ///      extensions 4 to max;
-  ///    }
-  ///  Note that the message cannot have any defined fields; MessageSets only
-  ///  have extensions.
+  /// The message must be defined exactly as follows:
+  ///   message Foo {
+  ///     option message_set_wire_format = true;
+  ///     extensions 4 to max;
+  ///   }
+  /// Note that the message cannot have any defined fields; MessageSets only
+  /// have extensions.
   ///
-  ///  All extensions of your type must be singular messages; e.g. they cannot
-  ///  be int32s, enums, or repeated messages.
+  /// All extensions of your type must be singular messages; e.g. they cannot
+  /// be int32s, enums, or repeated messages.
   ///
-  ///  Because this is an option, the above two restrictions are not enforced by
-  ///  the protocol compiler.
+  /// Because this is an option, the above two restrictions are not enforced by
+  /// the protocol compiler.
   @$pb.TagNumber(1)
   $core.bool get messageSetWireFormat => $_getBF(0);
   @$pb.TagNumber(1)
@@ -2224,27 +2224,27 @@
   @$pb.TagNumber(3)
   void clearDeprecated() => $_clearField(3);
 
-  ///  Whether the message is an automatically generated map entry type for the
-  ///  maps field.
+  /// Whether the message is an automatically generated map entry type for the
+  /// maps field.
   ///
-  ///  For maps fields:
-  ///      map<KeyType, ValueType> map_field = 1;
-  ///  The parsed descriptor looks like:
-  ///      message MapFieldEntry {
-  ///          option map_entry = true;
-  ///          optional KeyType key = 1;
-  ///          optional ValueType value = 2;
-  ///      }
-  ///      repeated MapFieldEntry map_field = 1;
+  /// For maps fields:
+  ///     map<KeyType, ValueType> map_field = 1;
+  /// The parsed descriptor looks like:
+  ///     message MapFieldEntry {
+  ///         option map_entry = true;
+  ///         optional KeyType key = 1;
+  ///         optional ValueType value = 2;
+  ///     }
+  ///     repeated MapFieldEntry map_field = 1;
   ///
-  ///  Implementations may choose not to generate the map_entry=true message, but
-  ///  use a native map in the target language to hold the keys and values.
-  ///  The reflection APIs in such implementations still need to work as
-  ///  if the field is a repeated message field.
+  /// Implementations may choose not to generate the map_entry=true message, but
+  /// use a native map in the target language to hold the keys and values.
+  /// The reflection APIs in such implementations still need to work as
+  /// if the field is a repeated message field.
   ///
-  ///  NOTE: Do not set the option in .proto files. Always use the maps syntax
-  ///  instead. The option should only be implicitly set by the proto compiler
-  ///  parser.
+  /// NOTE: Do not set the option in .proto files. Always use the maps syntax
+  /// instead. The option should only be implicitly set by the proto compiler
+  /// parser.
   @$pb.TagNumber(7)
   $core.bool get mapEntry => $_getBF(3);
   @$pb.TagNumber(7)
@@ -2400,34 +2400,34 @@
   @$pb.TagNumber(3)
   void clearDeprecated() => $_clearField(3);
 
-  ///  Should this field be parsed lazily?  Lazy applies only to message-type
-  ///  fields.  It means that when the outer message is initially parsed, the
-  ///  inner message's contents will not be parsed but instead stored in encoded
-  ///  form.  The inner message will actually be parsed when it is first accessed.
+  /// Should this field be parsed lazily?  Lazy applies only to message-type
+  /// fields.  It means that when the outer message is initially parsed, the
+  /// inner message's contents will not be parsed but instead stored in encoded
+  /// form.  The inner message will actually be parsed when it is first accessed.
   ///
-  ///  This is only a hint.  Implementations are free to choose whether to use
-  ///  eager or lazy parsing regardless of the value of this option.  However,
-  ///  setting this option true suggests that the protocol author believes that
-  ///  using lazy parsing on this field is worth the additional bookkeeping
-  ///  overhead typically needed to implement it.
+  /// This is only a hint.  Implementations are free to choose whether to use
+  /// eager or lazy parsing regardless of the value of this option.  However,
+  /// setting this option true suggests that the protocol author believes that
+  /// using lazy parsing on this field is worth the additional bookkeeping
+  /// overhead typically needed to implement it.
   ///
-  ///  This option does not affect the public interface of any generated code;
-  ///  all method signatures remain the same.  Furthermore, thread-safety of the
-  ///  interface is not affected by this option; const methods remain safe to
-  ///  call from multiple threads concurrently, while non-const methods continue
-  ///  to require exclusive access.
+  /// This option does not affect the public interface of any generated code;
+  /// all method signatures remain the same.  Furthermore, thread-safety of the
+  /// interface is not affected by this option; const methods remain safe to
+  /// call from multiple threads concurrently, while non-const methods continue
+  /// to require exclusive access.
   ///
   ///
-  ///  Note that implementations may choose not to check required fields within
-  ///  a lazy sub-message.  That is, calling IsInitialized() on the outer message
-  ///  may return true even if the inner message has missing required fields.
-  ///  This is necessary because otherwise the inner message would have to be
-  ///  parsed in order to perform the check, defeating the purpose of lazy
-  ///  parsing.  An implementation which chooses not to check required fields
-  ///  must be consistent about it.  That is, for any particular sub-message, the
-  ///  implementation must either *always* check its required fields, or *never*
-  ///  check its required fields, regardless of whether or not the message has
-  ///  been parsed.
+  /// Note that implementations may choose not to check required fields within
+  /// a lazy sub-message.  That is, calling IsInitialized() on the outer message
+  /// may return true even if the inner message has missing required fields.
+  /// This is necessary because otherwise the inner message would have to be
+  /// parsed in order to perform the check, defeating the purpose of lazy
+  /// parsing.  An implementation which chooses not to check required fields
+  /// must be consistent about it.  That is, for any particular sub-message, the
+  /// implementation must either *always* check its required fields, or *never*
+  /// check its required fields, regardless of whether or not the message has
+  /// been parsed.
   @$pb.TagNumber(5)
   $core.bool get lazy => $_getBF(3);
   @$pb.TagNumber(5)
@@ -2440,17 +2440,17 @@
   @$pb.TagNumber(5)
   void clearLazy() => $_clearField(5);
 
-  ///  The jstype option determines the JavaScript type used for values of the
-  ///  field.  The option is permitted only for 64 bit integral and fixed types
-  ///  (int64, uint64, sint64, fixed64, sfixed64).  A field with jstype JS_STRING
-  ///  is represented as JavaScript string, which avoids loss of precision that
-  ///  can happen when a large value is converted to a floating point JavaScript.
-  ///  Specifying JS_NUMBER for the jstype causes the generated JavaScript code to
-  ///  use the JavaScript "number" type.  The behavior of the default option
-  ///  JS_NORMAL is implementation dependent.
+  /// The jstype option determines the JavaScript type used for values of the
+  /// field.  The option is permitted only for 64 bit integral and fixed types
+  /// (int64, uint64, sint64, fixed64, sfixed64).  A field with jstype JS_STRING
+  /// is represented as JavaScript string, which avoids loss of precision that
+  /// can happen when a large value is converted to a floating point JavaScript.
+  /// Specifying JS_NUMBER for the jstype causes the generated JavaScript code to
+  /// use the JavaScript "number" type.  The behavior of the default option
+  /// JS_NORMAL is implementation dependent.
   ///
-  ///  This option is an enum to permit additional types to be added, e.g.
-  ///  goog.math.Integer.
+  /// This option is an enum to permit additional types to be added, e.g.
+  /// goog.math.Integer.
   @$pb.TagNumber(6)
   FieldOptions_JSType get jstype => $_getN(4);
   @$pb.TagNumber(6)
@@ -3211,29 +3211,29 @@
       $pb.GeneratedMessage.$_defaultFor<SourceCodeInfo_Location>(create);
   static SourceCodeInfo_Location? _defaultInstance;
 
-  ///  Identifies which part of the FileDescriptorProto was defined at this
-  ///  location.
+  /// Identifies which part of the FileDescriptorProto was defined at this
+  /// location.
   ///
-  ///  Each element is a field number or an index.  They form a path from
-  ///  the root FileDescriptorProto to the place where the definition.  For
-  ///  example, this path:
-  ///    [ 4, 3, 2, 7, 1 ]
-  ///  refers to:
-  ///    file.message_type(3)  // 4, 3
-  ///        .field(7)         // 2, 7
-  ///        .name()           // 1
-  ///  This is because FileDescriptorProto.message_type has field number 4:
-  ///    repeated DescriptorProto message_type = 4;
-  ///  and DescriptorProto.field has field number 2:
-  ///    repeated FieldDescriptorProto field = 2;
-  ///  and FieldDescriptorProto.name has field number 1:
-  ///    optional string name = 1;
+  /// Each element is a field number or an index.  They form a path from
+  /// the root FileDescriptorProto to the place where the definition.  For
+  /// example, this path:
+  ///   [ 4, 3, 2, 7, 1 ]
+  /// refers to:
+  ///   file.message_type(3)  // 4, 3
+  ///       .field(7)         // 2, 7
+  ///       .name()           // 1
+  /// This is because FileDescriptorProto.message_type has field number 4:
+  ///   repeated DescriptorProto message_type = 4;
+  /// and DescriptorProto.field has field number 2:
+  ///   repeated FieldDescriptorProto field = 2;
+  /// and FieldDescriptorProto.name has field number 1:
+  ///   optional string name = 1;
   ///
-  ///  Thus, the above path gives the location of a field name.  If we removed
-  ///  the last element:
-  ///    [ 4, 3, 2, 7 ]
-  ///  this path refers to the whole field declaration (from the beginning
-  ///  of the label to the terminating semicolon).
+  /// Thus, the above path gives the location of a field name.  If we removed
+  /// the last element:
+  ///   [ 4, 3, 2, 7 ]
+  /// this path refers to the whole field declaration (from the beginning
+  /// of the label to the terminating semicolon).
   @$pb.TagNumber(1)
   $pb.PbList<$core.int> get path => $_getList(0);
 
@@ -3245,53 +3245,53 @@
   @$pb.TagNumber(2)
   $pb.PbList<$core.int> get span => $_getList(1);
 
-  ///  If this SourceCodeInfo represents a complete declaration, these are any
-  ///  comments appearing before and after the declaration which appear to be
-  ///  attached to the declaration.
+  /// If this SourceCodeInfo represents a complete declaration, these are any
+  /// comments appearing before and after the declaration which appear to be
+  /// attached to the declaration.
   ///
-  ///  A series of line comments appearing on consecutive lines, with no other
-  ///  tokens appearing on those lines, will be treated as a single comment.
+  /// A series of line comments appearing on consecutive lines, with no other
+  /// tokens appearing on those lines, will be treated as a single comment.
   ///
-  ///  leading_detached_comments will keep paragraphs of comments that appear
-  ///  before (but not connected to) the current element. Each paragraph,
-  ///  separated by empty lines, will be one comment element in the repeated
-  ///  field.
+  /// leading_detached_comments will keep paragraphs of comments that appear
+  /// before (but not connected to) the current element. Each paragraph,
+  /// separated by empty lines, will be one comment element in the repeated
+  /// field.
   ///
-  ///  Only the comment content is provided; comment markers (e.g. //) are
-  ///  stripped out.  For block comments, leading whitespace and an asterisk
-  ///  will be stripped from the beginning of each line other than the first.
-  ///  Newlines are included in the output.
+  /// Only the comment content is provided; comment markers (e.g. //) are
+  /// stripped out.  For block comments, leading whitespace and an asterisk
+  /// will be stripped from the beginning of each line other than the first.
+  /// Newlines are included in the output.
   ///
-  ///  Examples:
+  /// Examples:
   ///
-  ///    optional int32 foo = 1;  // Comment attached to foo.
-  ///    // Comment attached to bar.
-  ///    optional int32 bar = 2;
+  ///   optional int32 foo = 1;  // Comment attached to foo.
+  ///   // Comment attached to bar.
+  ///   optional int32 bar = 2;
   ///
-  ///    optional string baz = 3;
-  ///    // Comment attached to baz.
-  ///    // Another line attached to baz.
+  ///   optional string baz = 3;
+  ///   // Comment attached to baz.
+  ///   // Another line attached to baz.
   ///
-  ///    // Comment attached to qux.
-  ///    //
-  ///    // Another line attached to qux.
-  ///    optional double qux = 4;
+  ///   // Comment attached to qux.
+  ///   //
+  ///   // Another line attached to qux.
+  ///   optional double qux = 4;
   ///
-  ///    // Detached comment for corge. This is not leading or trailing comments
-  ///    // to qux or corge because there are blank lines separating it from
-  ///    // both.
+  ///   // Detached comment for corge. This is not leading or trailing comments
+  ///   // to qux or corge because there are blank lines separating it from
+  ///   // both.
   ///
-  ///    // Detached comment for corge paragraph 2.
+  ///   // Detached comment for corge paragraph 2.
   ///
-  ///    optional string corge = 5;
-  ///    /* Block comment attached
-  ///     * to corge.  Leading asterisks
-  ///     * will be removed. */
-  ///    /* Block comment attached to
-  ///     * grault. */
-  ///    optional int32 grault = 6;
+  ///   optional string corge = 5;
+  ///   /* Block comment attached
+  ///    * to corge.  Leading asterisks
+  ///    * will be removed. */
+  ///   /* Block comment attached to
+  ///    * grault. */
+  ///   optional int32 grault = 6;
   ///
-  ///    // ignored detached comments.
+  ///   // ignored detached comments.
   @$pb.TagNumber(3)
   $core.String get leadingComments => $_getSZ(2);
   @$pb.TagNumber(3)
@@ -3373,49 +3373,49 @@
       $pb.GeneratedMessage.$_defaultFor<SourceCodeInfo>(create);
   static SourceCodeInfo? _defaultInstance;
 
-  ///  A Location identifies a piece of source code in a .proto file which
-  ///  corresponds to a particular definition.  This information is intended
-  ///  to be useful to IDEs, code indexers, documentation generators, and similar
-  ///  tools.
+  /// A Location identifies a piece of source code in a .proto file which
+  /// corresponds to a particular definition.  This information is intended
+  /// to be useful to IDEs, code indexers, documentation generators, and similar
+  /// tools.
   ///
-  ///  For example, say we have a file like:
-  ///    message Foo {
-  ///      optional string foo = 1;
-  ///    }
-  ///  Let's look at just the field definition:
-  ///    optional string foo = 1;
-  ///    ^       ^^     ^^  ^  ^^^
-  ///    a       bc     de  f  ghi
-  ///  We have the following locations:
-  ///    span   path               represents
-  ///    [a,i)  [ 4, 0, 2, 0 ]     The whole field definition.
-  ///    [a,b)  [ 4, 0, 2, 0, 4 ]  The label (optional).
-  ///    [c,d)  [ 4, 0, 2, 0, 5 ]  The type (string).
-  ///    [e,f)  [ 4, 0, 2, 0, 1 ]  The name (foo).
-  ///    [g,h)  [ 4, 0, 2, 0, 3 ]  The number (1).
+  /// For example, say we have a file like:
+  ///   message Foo {
+  ///     optional string foo = 1;
+  ///   }
+  /// Let's look at just the field definition:
+  ///   optional string foo = 1;
+  ///   ^       ^^     ^^  ^  ^^^
+  ///   a       bc     de  f  ghi
+  /// We have the following locations:
+  ///   span   path               represents
+  ///   [a,i)  [ 4, 0, 2, 0 ]     The whole field definition.
+  ///   [a,b)  [ 4, 0, 2, 0, 4 ]  The label (optional).
+  ///   [c,d)  [ 4, 0, 2, 0, 5 ]  The type (string).
+  ///   [e,f)  [ 4, 0, 2, 0, 1 ]  The name (foo).
+  ///   [g,h)  [ 4, 0, 2, 0, 3 ]  The number (1).
   ///
-  ///  Notes:
-  ///  - A location may refer to a repeated field itself (i.e. not to any
-  ///    particular index within it).  This is used whenever a set of elements are
-  ///    logically enclosed in a single code segment.  For example, an entire
-  ///    extend block (possibly containing multiple extension definitions) will
-  ///    have an outer location whose path refers to the "extensions" repeated
-  ///    field without an index.
-  ///  - Multiple locations may have the same path.  This happens when a single
-  ///    logical declaration is spread out across multiple places.  The most
-  ///    obvious example is the "extend" block again -- there may be multiple
-  ///    extend blocks in the same scope, each of which will have the same path.
-  ///  - A location's span is not always a subset of its parent's span.  For
-  ///    example, the "extendee" of an extension declaration appears at the
-  ///    beginning of the "extend" block and is shared by all extensions within
-  ///    the block.
-  ///  - Just because a location's span is a subset of some other location's span
-  ///    does not mean that it is a descendant.  For example, a "group" defines
-  ///    both a type and a field in a single declaration.  Thus, the locations
-  ///    corresponding to the type and field and their components will overlap.
-  ///  - Code which tries to interpret locations should probably be designed to
-  ///    ignore those that it doesn't understand, as more types of locations could
-  ///    be recorded in the future.
+  /// Notes:
+  /// - A location may refer to a repeated field itself (i.e. not to any
+  ///   particular index within it).  This is used whenever a set of elements are
+  ///   logically enclosed in a single code segment.  For example, an entire
+  ///   extend block (possibly containing multiple extension definitions) will
+  ///   have an outer location whose path refers to the "extensions" repeated
+  ///   field without an index.
+  /// - Multiple locations may have the same path.  This happens when a single
+  ///   logical declaration is spread out across multiple places.  The most
+  ///   obvious example is the "extend" block again -- there may be multiple
+  ///   extend blocks in the same scope, each of which will have the same path.
+  /// - A location's span is not always a subset of its parent's span.  For
+  ///   example, the "extendee" of an extension declaration appears at the
+  ///   beginning of the "extend" block and is shared by all extensions within
+  ///   the block.
+  /// - Just because a location's span is a subset of some other location's span
+  ///   does not mean that it is a descendant.  For example, a "group" defines
+  ///   both a type and a field in a single declaration.  Thus, the locations
+  ///   corresponding to the type and field and their components will overlap.
+  /// - Code which tries to interpret locations should probably be designed to
+  ///   ignore those that it doesn't understand, as more types of locations could
+  ///   be recorded in the future.
   @$pb.TagNumber(1)
   $pb.PbList<SourceCodeInfo_Location> get location => $_getList(0);
 }
diff --git a/protoc_plugin/lib/src/generated/plugin.pb.dart b/protoc_plugin/lib/src/generated/plugin.pb.dart
index e50ab70..30c8a8d 100644
--- a/protoc_plugin/lib/src/generated/plugin.pb.dart
+++ b/protoc_plugin/lib/src/generated/plugin.pb.dart
@@ -236,20 +236,20 @@
   @$pb.TagNumber(3)
   Version ensureCompilerVersion() => $_ensure(2);
 
-  ///  FileDescriptorProtos for all files in files_to_generate and everything
-  ///  they import.  The files will appear in topological order, so each file
-  ///  appears before any file that imports it.
+  /// FileDescriptorProtos for all files in files_to_generate and everything
+  /// they import.  The files will appear in topological order, so each file
+  /// appears before any file that imports it.
   ///
-  ///  protoc guarantees that all proto_files will be written after
-  ///  the fields above, even though this is not technically guaranteed by the
-  ///  protobuf wire format.  This theoretically could allow a plugin to stream
-  ///  in the FileDescriptorProtos and handle them one by one rather than read
-  ///  the entire set into memory at once.  However, as of this writing, this
-  ///  is not similarly optimized on protoc's end -- it will store all fields in
-  ///  memory at once before sending them to the plugin.
+  /// protoc guarantees that all proto_files will be written after
+  /// the fields above, even though this is not technically guaranteed by the
+  /// protobuf wire format.  This theoretically could allow a plugin to stream
+  /// in the FileDescriptorProtos and handle them one by one rather than read
+  /// the entire set into memory at once.  However, as of this writing, this
+  /// is not similarly optimized on protoc's end -- it will store all fields in
+  /// memory at once before sending them to the plugin.
   ///
-  ///  Type names of fields and extensions in the FileDescriptorProto are always
-  ///  fully qualified.
+  /// Type names of fields and extensions in the FileDescriptorProto are always
+  /// fully qualified.
   @$pb.TagNumber(15)
   $pb.PbList<$0.FileDescriptorProto> get protoFile => $_getList(3);
 }
@@ -323,17 +323,17 @@
       $pb.GeneratedMessage.$_defaultFor<CodeGeneratorResponse_File>(create);
   static CodeGeneratorResponse_File? _defaultInstance;
 
-  ///  The file name, relative to the output directory.  The name must not
-  ///  contain "." or ".." components and must be relative, not be absolute (so,
-  ///  the file cannot lie outside the output directory).  "/" must be used as
-  ///  the path separator, not "\".
+  /// The file name, relative to the output directory.  The name must not
+  /// contain "." or ".." components and must be relative, not be absolute (so,
+  /// the file cannot lie outside the output directory).  "/" must be used as
+  /// the path separator, not "\".
   ///
-  ///  If the name is omitted, the content will be appended to the previous
-  ///  file.  This allows the generator to break large files into small chunks,
-  ///  and allows the generated text to be streamed back to protoc so that large
-  ///  files need not reside completely in memory at one time.  Note that as of
-  ///  this writing protoc does not optimize for this -- it will read the entire
-  ///  CodeGeneratorResponse before writing files to disk.
+  /// If the name is omitted, the content will be appended to the previous
+  /// file.  This allows the generator to break large files into small chunks,
+  /// and allows the generated text to be streamed back to protoc so that large
+  /// files need not reside completely in memory at one time.  Note that as of
+  /// this writing protoc does not optimize for this -- it will read the entire
+  /// CodeGeneratorResponse before writing files to disk.
   @$pb.TagNumber(1)
   $core.String get name => $_getSZ(0);
   @$pb.TagNumber(1)
@@ -346,43 +346,43 @@
   @$pb.TagNumber(1)
   void clearName() => $_clearField(1);
 
-  ///  If non-empty, indicates that the named file should already exist, and the
-  ///  content here is to be inserted into that file at a defined insertion
-  ///  point.  This feature allows a code generator to extend the output
-  ///  produced by another code generator.  The original generator may provide
-  ///  insertion points by placing special annotations in the file that look
-  ///  like:
-  ///    @@protoc_insertion_point(NAME)
-  ///  The annotation can have arbitrary text before and after it on the line,
-  ///  which allows it to be placed in a comment.  NAME should be replaced with
-  ///  an identifier naming the point -- this is what other generators will use
-  ///  as the insertion_point.  Code inserted at this point will be placed
-  ///  immediately above the line containing the insertion point (thus multiple
-  ///  insertions to the same point will come out in the order they were added).
-  ///  The double-@ is intended to make it unlikely that the generated code
-  ///  could contain things that look like insertion points by accident.
+  /// If non-empty, indicates that the named file should already exist, and the
+  /// content here is to be inserted into that file at a defined insertion
+  /// point.  This feature allows a code generator to extend the output
+  /// produced by another code generator.  The original generator may provide
+  /// insertion points by placing special annotations in the file that look
+  /// like:
+  ///   @@protoc_insertion_point(NAME)
+  /// The annotation can have arbitrary text before and after it on the line,
+  /// which allows it to be placed in a comment.  NAME should be replaced with
+  /// an identifier naming the point -- this is what other generators will use
+  /// as the insertion_point.  Code inserted at this point will be placed
+  /// immediately above the line containing the insertion point (thus multiple
+  /// insertions to the same point will come out in the order they were added).
+  /// The double-@ is intended to make it unlikely that the generated code
+  /// could contain things that look like insertion points by accident.
   ///
-  ///  For example, the C++ code generator places the following line in the
-  ///  .pb.h files that it generates:
-  ///    // @@protoc_insertion_point(namespace_scope)
-  ///  This line appears within the scope of the file's package namespace, but
-  ///  outside of any particular class.  Another plugin can then specify the
-  ///  insertion_point "namespace_scope" to generate additional classes or
-  ///  other declarations that should be placed in this scope.
+  /// For example, the C++ code generator places the following line in the
+  /// .pb.h files that it generates:
+  ///   // @@protoc_insertion_point(namespace_scope)
+  /// This line appears within the scope of the file's package namespace, but
+  /// outside of any particular class.  Another plugin can then specify the
+  /// insertion_point "namespace_scope" to generate additional classes or
+  /// other declarations that should be placed in this scope.
   ///
-  ///  Note that if the line containing the insertion point begins with
-  ///  whitespace, the same whitespace will be added to every line of the
-  ///  inserted text.  This is useful for languages like Python, where
-  ///  indentation matters.  In these languages, the insertion point comment
-  ///  should be indented the same amount as any inserted code will need to be
-  ///  in order to work correctly in that context.
+  /// Note that if the line containing the insertion point begins with
+  /// whitespace, the same whitespace will be added to every line of the
+  /// inserted text.  This is useful for languages like Python, where
+  /// indentation matters.  In these languages, the insertion point comment
+  /// should be indented the same amount as any inserted code will need to be
+  /// in order to work correctly in that context.
   ///
-  ///  The code generator that generates the initial file and the one which
-  ///  inserts into it must both run as part of a single invocation of protoc.
-  ///  Code generators are executed in the order in which they appear on the
-  ///  command line.
+  /// The code generator that generates the initial file and the one which
+  /// inserts into it must both run as part of a single invocation of protoc.
+  /// Code generators are executed in the order in which they appear on the
+  /// command line.
   ///
-  ///  If |insertion_point| is present, |name| must also be present.
+  /// If |insertion_point| is present, |name| must also be present.
   @$pb.TagNumber(2)
   $core.String get insertionPoint => $_getSZ(1);
   @$pb.TagNumber(2)
@@ -492,14 +492,14 @@
       $pb.GeneratedMessage.$_defaultFor<CodeGeneratorResponse>(create);
   static CodeGeneratorResponse? _defaultInstance;
 
-  ///  Error message.  If non-empty, code generation failed.  The plugin process
-  ///  should exit with status code zero even if it reports an error in this way.
+  /// Error message.  If non-empty, code generation failed.  The plugin process
+  /// should exit with status code zero even if it reports an error in this way.
   ///
-  ///  This should be used to indicate errors in .proto files which prevent the
-  ///  code generator from generating correct code.  Errors which indicate a
-  ///  problem in protoc itself -- such as the input CodeGeneratorRequest being
-  ///  unparseable -- should be reported by writing a message to stderr and
-  ///  exiting with a non-zero status code.
+  /// This should be used to indicate errors in .proto files which prevent the
+  /// code generator from generating correct code.  Errors which indicate a
+  /// problem in protoc itself -- such as the input CodeGeneratorRequest being
+  /// unparseable -- should be reported by writing a message to stderr and
+  /// exiting with a non-zero status code.
   @$pb.TagNumber(1)
   $core.String get error => $_getSZ(0);
   @$pb.TagNumber(1)
diff --git a/protoc_plugin/lib/src/grpc_generator.dart b/protoc_plugin/lib/src/grpc_generator.dart
index 1d9257c..85ad5e9 100644
--- a/protoc_plugin/lib/src/grpc_generator.dart
+++ b/protoc_plugin/lib/src/grpc_generator.dart
@@ -10,16 +10,18 @@
   /// The generator of the .pb.dart file that will contain this service.
   final FileGenerator fileGen;
 
+  final int _serviceIndex;
+
   /// The message types needed directly by this service.
   ///
   /// The key is the fully qualified name.
   /// Populated by [resolve].
-  final _deps = <String, MessageGenerator>{};
+  final Map<String, MessageGenerator> _deps = {};
 
   /// Maps each undefined type to a string describing its location.
   ///
   /// Populated by [resolve].
-  final _undefinedDeps = <String, String>{};
+  final Map<String, String> _undefinedDeps = {};
 
   /// Fully-qualified gRPC service name.
   late final String _fullServiceName;
@@ -31,9 +33,15 @@
   late final String _serviceClassname;
 
   /// List of gRPC methods.
-  final _methods = <_GrpcMethod>[];
+  final List<_GrpcMethod> _methods = [];
 
-  GrpcServiceGenerator(this._descriptor, this.fileGen) {
+  late final List<int> _serviceDescriptorPath = [
+    ...fileGen.fieldPath,
+    ClientApiGenerator.fileDescriptorServiceTag,
+    _serviceIndex,
+  ];
+
+  GrpcServiceGenerator(this._descriptor, this.fileGen, this._serviceIndex) {
     final name = _descriptor.name;
     final package = fileGen.package;
 
@@ -106,6 +114,10 @@
   }
 
   void _generateClient(IndentingWriter out) {
+    final commentBlock = fileGen.commentBlock(_serviceDescriptorPath);
+    if (commentBlock != null) {
+      out.println(commentBlock);
+    }
     if (_descriptor.options.deprecated) {
       out.println(
           "@$coreImportPrefix.Deprecated('This service is deprecated')");
@@ -118,8 +130,8 @@
       out.println();
       out.println(
           '$_clientClassname(super.channel, {super.options, super.interceptors});');
-      for (final method in _methods) {
-        method.generateClientStub(out);
+      for (var i = 0; i < _methods.length; i++) {
+        _methods[i].generateClientStub(out, this, i);
       }
     });
   }
@@ -228,8 +240,22 @@
         '    ($coreImportPrefix.List<$coreImportPrefix.int> value) => $_responseType.fromBuffer(value));');
   }
 
-  void generateClientStub(IndentingWriter out) {
+  List<int> _methodDescriptorPath(GrpcServiceGenerator generator, int index) {
+    return [
+      ...generator._serviceDescriptorPath,
+      ClientApiGenerator.serviceDescriptorMethodTag,
+      index,
+    ];
+  }
+
+  void generateClientStub(IndentingWriter out,
+      GrpcServiceGenerator serviceGenerator, int methodIndex) {
     out.println();
+    final commentBlock = serviceGenerator.fileGen
+        .commentBlock(_methodDescriptorPath(serviceGenerator, methodIndex));
+    if (commentBlock != null) {
+      out.println(commentBlock);
+    }
     if (_deprecated) {
       out.println(
           '@$coreImportPrefix.Deprecated(\'This method is deprecated\')');
diff --git a/protoc_plugin/lib/src/message_generator.dart b/protoc_plugin/lib/src/message_generator.dart
index 8746559..f26cec3 100644
--- a/protoc_plugin/lib/src/message_generator.dart
+++ b/protoc_plugin/lib/src/message_generator.dart
@@ -68,8 +68,7 @@
   final List<int> _fieldPathSegment;
 
   @override
-  late final List<int> fieldPath = List.from(parent!.fieldPath!)
-    ..addAll(_fieldPathSegment);
+  late final List<int> fieldPath = [...parent!.fieldPath, ..._fieldPathSegment];
 
   // populated by resolve()
   late List<ProtobufField> _fieldList;
diff --git a/protoc_plugin/lib/src/shared.dart b/protoc_plugin/lib/src/shared.dart
index 446f12d..c2c729d 100644
--- a/protoc_plugin/lib/src/shared.dart
+++ b/protoc_plugin/lib/src/shared.dart
@@ -66,6 +66,9 @@
 /// This is the internal method for [FileDescriptorProtoExt.commentBlock],
 /// public to be able to test.
 String? toDartComment(String value) {
+  // TODO: Handle converting proto references to Dart references.
+  // "[Foo][google.firestore.v1.Foo]" => to either "`Foo`" or "[Foo]".
+
   if (value.isEmpty) return null;
 
   var lines = LineSplitter.split(value).toList();
@@ -75,8 +78,10 @@
   final leadingSpaces = _leadingSpaces.firstMatch(lines.first);
   if (leadingSpaces != null) {
     final prefix = leadingSpaces.group(0)!;
-    if (lines.every((element) => element.startsWith(prefix))) {
-      lines = lines.map((e) => e.substring(prefix.length)).toList();
+    if (lines.every((line) => line.isEmpty || line.startsWith(prefix))) {
+      lines = lines
+          .map((line) => line.isEmpty ? line : line.substring(prefix.length))
+          .toList();
     }
   }
 
diff --git a/protoc_plugin/pubspec.yaml b/protoc_plugin/pubspec.yaml
index ffd1465..c1c12a2 100644
--- a/protoc_plugin/pubspec.yaml
+++ b/protoc_plugin/pubspec.yaml
@@ -1,5 +1,5 @@
 name: protoc_plugin
-version: 22.1.0
+version: 22.1.1-wip
 description: A protobuf protoc compiler plugin used to generate Dart code.
 repository: https://github.com/google/protobuf.dart/tree/master/protoc_plugin
 
diff --git a/protoc_plugin/test/shared_test.dart b/protoc_plugin/test/shared_test.dart
index f15f471..3c8fb07 100644
--- a/protoc_plugin/test/shared_test.dart
+++ b/protoc_plugin/test/shared_test.dart
@@ -26,5 +26,19 @@
 /// with one indent - trailing whitespace removed''',
       );
     });
+
+    test('indent with blank lines', () {
+      expect(
+        toDartComment('''
+ This is indented.
+
+ This is indented.
+'''),
+        '''
+/// This is indented.
+///
+/// This is indented.''',
+      );
+    });
   });
 }