Send cursor position and linked edit groups down with quick fixes (#442)

diff --git a/doc/generated/dartservices.dart b/doc/generated/dartservices.dart
index 383784a..a83bd79 100644
--- a/doc/generated/dartservices.dart
+++ b/doc/generated/dartservices.dart
@@ -471,7 +471,9 @@
 
 class CandidateFix {
   core.List<SourceEdit> edits;
+  core.List<LinkedEditGroup> linkedEditGroups;
   core.String message;
+  core.int selectionOffset;
 
   CandidateFix();
 
@@ -481,9 +483,17 @@
           .map<SourceEdit>((value) => new SourceEdit.fromJson(value))
           .toList();
     }
+    if (_json.containsKey("linkedEditGroups")) {
+      linkedEditGroups = (_json["linkedEditGroups"] as core.List)
+          .map<LinkedEditGroup>((value) => new LinkedEditGroup.fromJson(value))
+          .toList();
+    }
     if (_json.containsKey("message")) {
       message = _json["message"];
     }
+    if (_json.containsKey("selectionOffset")) {
+      selectionOffset = _json["selectionOffset"];
+    }
   }
 
   core.Map<core.String, core.Object> toJson() {
@@ -492,9 +502,16 @@
     if (edits != null) {
       _json["edits"] = edits.map((value) => (value).toJson()).toList();
     }
+    if (linkedEditGroups != null) {
+      _json["linkedEditGroups"] =
+          linkedEditGroups.map((value) => (value).toJson()).toList();
+    }
     if (message != null) {
       _json["message"] = message;
     }
+    if (selectionOffset != null) {
+      _json["selectionOffset"] = selectionOffset;
+    }
     return _json;
   }
 }
@@ -703,6 +720,82 @@
   }
 }
 
+class LinkedEditGroup {
+  /// The length of the regions that should be edited simultaneously.
+  core.int length;
+
+  /// The positions of the regions that should be edited simultaneously.
+  core.List<core.int> positions;
+
+  /// Pre-computed suggestions for what every region might want to be changed
+  /// to.
+  core.List<LinkedEditSuggestion> suggestions;
+
+  LinkedEditGroup();
+
+  LinkedEditGroup.fromJson(core.Map _json) {
+    if (_json.containsKey("length")) {
+      length = _json["length"];
+    }
+    if (_json.containsKey("positions")) {
+      positions = (_json["positions"] as core.List).cast<core.int>();
+    }
+    if (_json.containsKey("suggestions")) {
+      suggestions = (_json["suggestions"] as core.List)
+          .map<LinkedEditSuggestion>(
+              (value) => new LinkedEditSuggestion.fromJson(value))
+          .toList();
+    }
+  }
+
+  core.Map<core.String, core.Object> toJson() {
+    final core.Map<core.String, core.Object> _json =
+        new core.Map<core.String, core.Object>();
+    if (length != null) {
+      _json["length"] = length;
+    }
+    if (positions != null) {
+      _json["positions"] = positions;
+    }
+    if (suggestions != null) {
+      _json["suggestions"] =
+          suggestions.map((value) => (value).toJson()).toList();
+    }
+    return _json;
+  }
+}
+
+class LinkedEditSuggestion {
+  /// The kind of value being proposed.
+  core.String kind;
+
+  /// The value that could be used to replace all of the linked edit regions.
+  core.String value;
+
+  LinkedEditSuggestion();
+
+  LinkedEditSuggestion.fromJson(core.Map _json) {
+    if (_json.containsKey("kind")) {
+      kind = _json["kind"];
+    }
+    if (_json.containsKey("value")) {
+      value = _json["value"];
+    }
+  }
+
+  core.Map<core.String, core.Object> toJson() {
+    final core.Map<core.String, core.Object> _json =
+        new core.Map<core.String, core.Object>();
+    if (kind != null) {
+      _json["kind"] = kind;
+    }
+    if (value != null) {
+      _json["value"] = value;
+    }
+    return _json;
+  }
+}
+
 class ProblemAndFixes {
   core.List<CandidateFix> fixes;
   core.int length;
diff --git a/doc/generated/dartservices.json b/doc/generated/dartservices.json
index 31f3744..172319c 100644
--- a/doc/generated/dartservices.json
+++ b/doc/generated/dartservices.json
@@ -1,6 +1,6 @@
 {
  "kind": "discovery#restDescription",
- "etag": "9511ad5e62f9a85d39dbd491c411705fd3876af8",
+ "etag": "3782b4a662b81bc2c7a01a7d5e7e9021a4debd44",
  "discoveryVersion": "v1",
  "id": "dartservices:v1",
  "name": "dartservices",
@@ -189,6 +189,16 @@
      "items": {
       "$ref": "SourceEdit"
      }
+    },
+    "selectionOffset": {
+     "type": "integer",
+     "format": "int32"
+    },
+    "linkedEditGroups": {
+     "type": "array",
+     "items": {
+      "$ref": "LinkedEditGroup"
+     }
     }
    }
   },
@@ -209,6 +219,46 @@
     }
    }
   },
+  "LinkedEditGroup": {
+   "id": "LinkedEditGroup",
+   "type": "object",
+   "properties": {
+    "positions": {
+     "type": "array",
+     "description": "The positions of the regions that should be edited simultaneously.",
+     "items": {
+      "type": "integer",
+      "format": "int32"
+     }
+    },
+    "length": {
+     "type": "integer",
+     "description": "The length of the regions that should be edited simultaneously.",
+     "format": "int32"
+    },
+    "suggestions": {
+     "type": "array",
+     "description": "Pre-computed suggestions for what every region might want to be changed to.",
+     "items": {
+      "$ref": "LinkedEditSuggestion"
+     }
+    }
+   }
+  },
+  "LinkedEditSuggestion": {
+   "id": "LinkedEditSuggestion",
+   "type": "object",
+   "properties": {
+    "value": {
+     "type": "string",
+     "description": "The value that could be used to replace all of the linked edit regions."
+    },
+    "kind": {
+     "type": "string",
+     "description": "The kind of value being proposed."
+    }
+   }
+  },
   "AssistsResponse": {
    "id": "AssistsResponse",
    "type": "object",
diff --git a/lib/src/analysis_server.dart b/lib/src/analysis_server.dart
index 671ca68..952d874 100644
--- a/lib/src/analysis_server.dart
+++ b/lib/src/analysis_server.dart
@@ -347,14 +347,34 @@
           return api.SourceEdit.fromChanges(
               sourceEdit.offset, sourceEdit.length, sourceEdit.replacement);
         }).toList();
-        assists.add(
-            api.CandidateFix.fromEdits(sourceChange.message, apiSourceEdits));
+
+        assists.add(api.CandidateFix.fromEdits(
+          sourceChange.message,
+          apiSourceEdits,
+          sourceChange.selection?.offset,
+          _convertLinkedEditGroups(sourceChange.linkedEditGroups),
+        ));
       }
     }
 
     return assists;
   }
 
+  /// Convert a list of the analysis server's [LinkedEditGroup]s into the API's
+  /// equivalent.
+  static List<api.LinkedEditGroup> _convertLinkedEditGroups(
+      List<LinkedEditGroup> groups) {
+    return groups?.map<api.LinkedEditGroup>((g) {
+      return api.LinkedEditGroup(
+        g.positions?.map((p) => p.offset)?.toList(),
+        g.length,
+        g.suggestions
+            ?.map((s) => api.LinkedEditSuggestion(s.value, s.kind))
+            ?.toList(),
+      );
+    })?.toList();
+  }
+
   /// Cleanly shutdown the Analysis Server.
   Future<dynamic> shutdown() {
     // TODO(jcollins-g): calling dispose() sometimes prevents
diff --git a/lib/src/api_classes.dart b/lib/src/api_classes.dart
index b907e4b..cfca8ec 100644
--- a/lib/src/api_classes.dart
+++ b/lib/src/api_classes.dart
@@ -183,14 +183,52 @@
       [this.fixes, this.problemMessage, this.offset, this.length]);
 }
 
+class LinkedEditSuggestion {
+  @ApiProperty(
+      description: 'The value that could be used to replace all of the linked '
+          'edit regions.')
+  final String value;
+
+  @ApiProperty(description: 'The kind of value being proposed.')
+  final String kind;
+
+  LinkedEditSuggestion(this.value, this.kind);
+}
+
+class LinkedEditGroup {
+  @ApiProperty(
+      description: 'The positions of the regions that should be edited '
+          'simultaneously.')
+  final List<int> positions;
+
+  @ApiProperty(
+      description: 'The length of the regions that should be edited '
+          'simultaneously.')
+  final int length;
+
+  @ApiProperty(
+      description: 'Pre-computed suggestions for what every region might want '
+          'to be changed to.')
+  final List<LinkedEditSuggestion> suggestions;
+
+  LinkedEditGroup(this.positions, this.length, this.suggestions);
+}
+
 /// Represents a possible way of solving an Analysis Problem.
 class CandidateFix {
   final String message;
   final List<SourceEdit> edits;
+  final int selectionOffset;
+  final List<LinkedEditGroup> linkedEditGroups;
 
   CandidateFix() : this.fromEdits();
 
-  CandidateFix.fromEdits([this.message, this.edits]);
+  CandidateFix.fromEdits([
+    this.message,
+    this.edits,
+    this.selectionOffset,
+    this.linkedEditGroups,
+  ]);
 }
 
 /// Represents a reformatting of the code.
@@ -231,6 +269,7 @@
 /// The response from the `/assists` service call.
 class AssistsResponse {
   final List<CandidateFix> assists;
+
   AssistsResponse(this.assists);
 }