Revise the analysis server edit.dartfix protocol

This updates the edit.dartfix protocol to separate location from the text
describing the changes so that the dartfix client can choose
what location information to display and how it should be displayed.

Change-Id: Ic56a4cb187538571d60136d6de9265d879fccb53
Reviewed-on: https://dart-review.googlesource.com/c/81780
Commit-Queue: Dan Rubel <danrubel@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/doc/api.html b/pkg/analysis_server/doc/api.html
index 15e83c4..5af3aa0 100644
--- a/pkg/analysis_server/doc/api.html
+++ b/pkg/analysis_server/doc/api.html
@@ -2660,6 +2660,7 @@
   
   
   
+  
 <dl><dt class="typeDefinition"><a name="type_AddContentOverlay">AddContentOverlay: object</a></dt><dd>
     <p>
       A directive to begin overlaying the contents of a file. The supplied
@@ -4379,9 +4380,9 @@
       </dd></dl></dd><dt class="typeDefinition"><a name="type_RuntimeCompletionExpression">RuntimeCompletionExpression: object</a></dt><dd>
     <p>
       An expression for which we want to know its runtime type.
-      In expressions like `a.b.c.where((e) =&gt; e.^)` we want to know the
-      runtime type of `a.b.c` to enforce it statically at the time when we
-      compute completion suggestions, and get better type for `e`.
+      In expressions like 'a.b.c.where((e) =&gt; e.^)' we want to know the
+      runtime type of 'a.b.c' to enforce it statically at the time when we
+      compute completion suggestions, and get better type for 'e'.
     </p>
     
   <dl><dt class="field"><b>offset: int</b></dt><dd>
diff --git a/pkg/analysis_server/lib/protocol/protocol_constants.dart b/pkg/analysis_server/lib/protocol/protocol_constants.dart
index 9d85d08..1280460 100644
--- a/pkg/analysis_server/lib/protocol/protocol_constants.dart
+++ b/pkg/analysis_server/lib/protocol/protocol_constants.dart
@@ -173,11 +173,10 @@
 const String EDIT_REQUEST_ORGANIZE_DIRECTIVES_FILE = 'file';
 const String EDIT_REQUEST_SORT_MEMBERS = 'edit.sortMembers';
 const String EDIT_REQUEST_SORT_MEMBERS_FILE = 'file';
-const String EDIT_RESPONSE_DARTFIX_DESCRIPTION_OF_FIXES = 'descriptionOfFixes';
-const String EDIT_RESPONSE_DARTFIX_FIXES = 'fixes';
+const String EDIT_RESPONSE_DARTFIX_EDITS = 'edits';
 const String EDIT_RESPONSE_DARTFIX_HAS_ERRORS = 'hasErrors';
-const String EDIT_RESPONSE_DARTFIX_OTHER_RECOMMENDATIONS =
-    'otherRecommendations';
+const String EDIT_RESPONSE_DARTFIX_OTHER_SUGGESTIONS = 'otherSuggestions';
+const String EDIT_RESPONSE_DARTFIX_SUGGESTIONS = 'suggestions';
 const String EDIT_RESPONSE_FORMAT_EDITS = 'edits';
 const String EDIT_RESPONSE_FORMAT_SELECTION_LENGTH = 'selectionLength';
 const String EDIT_RESPONSE_FORMAT_SELECTION_OFFSET = 'selectionOffset';
diff --git a/pkg/analysis_server/lib/protocol/protocol_generated.dart b/pkg/analysis_server/lib/protocol/protocol_generated.dart
index cb7d946..3a0a480 100644
--- a/pkg/analysis_server/lib/protocol/protocol_generated.dart
+++ b/pkg/analysis_server/lib/protocol/protocol_generated.dart
@@ -5932,6 +5932,105 @@
 }
 
 /**
+ * DartFixSuggestion
+ *
+ * {
+ *   "description": String
+ *   "location": optional Location
+ * }
+ *
+ * Clients may not extend, implement or mix-in this class.
+ */
+class DartFixSuggestion implements HasToJson {
+  String _description;
+
+  Location _location;
+
+  /**
+   * A human readable description of the suggested change.
+   */
+  String get description => _description;
+
+  /**
+   * A human readable description of the suggested change.
+   */
+  void set description(String value) {
+    assert(value != null);
+    this._description = value;
+  }
+
+  /**
+   * The location of the suggested change.
+   */
+  Location get location => _location;
+
+  /**
+   * The location of the suggested change.
+   */
+  void set location(Location value) {
+    this._location = value;
+  }
+
+  DartFixSuggestion(String description, {Location location}) {
+    this.description = description;
+    this.location = location;
+  }
+
+  factory DartFixSuggestion.fromJson(
+      JsonDecoder jsonDecoder, String jsonPath, Object json) {
+    if (json == null) {
+      json = {};
+    }
+    if (json is Map) {
+      String description;
+      if (json.containsKey("description")) {
+        description = jsonDecoder.decodeString(
+            jsonPath + ".description", json["description"]);
+      } else {
+        throw jsonDecoder.mismatch(jsonPath, "description");
+      }
+      Location location;
+      if (json.containsKey("location")) {
+        location = new Location.fromJson(
+            jsonDecoder, jsonPath + ".location", json["location"]);
+      }
+      return new DartFixSuggestion(description, location: location);
+    } else {
+      throw jsonDecoder.mismatch(jsonPath, "DartFixSuggestion", json);
+    }
+  }
+
+  @override
+  Map<String, dynamic> toJson() {
+    Map<String, dynamic> result = {};
+    result["description"] = description;
+    if (location != null) {
+      result["location"] = location.toJson();
+    }
+    return result;
+  }
+
+  @override
+  String toString() => json.encode(toJson());
+
+  @override
+  bool operator ==(other) {
+    if (other is DartFixSuggestion) {
+      return description == other.description && location == other.location;
+    }
+    return false;
+  }
+
+  @override
+  int get hashCode {
+    int hash = 0;
+    hash = JenkinsSmiHash.combine(hash, description.hashCode);
+    hash = JenkinsSmiHash.combine(hash, location.hashCode);
+    return JenkinsSmiHash.finish(hash);
+  }
+}
+
+/**
  * diagnostic.getDiagnostics params
  *
  * Clients may not extend, implement or mix-in this class.
@@ -6268,60 +6367,60 @@
  * edit.dartfix result
  *
  * {
- *   "descriptionOfFixes": List<String>
- *   "otherRecommendations": List<String>
+ *   "suggestions": List<DartFixSuggestion>
+ *   "otherSuggestions": List<DartFixSuggestion>
  *   "hasErrors": bool
- *   "fixes": List<SourceFileEdit>
+ *   "edits": List<SourceFileEdit>
  * }
  *
  * Clients may not extend, implement or mix-in this class.
  */
 class EditDartfixResult implements ResponseResult {
-  List<String> _descriptionOfFixes;
+  List<DartFixSuggestion> _suggestions;
 
-  List<String> _otherRecommendations;
+  List<DartFixSuggestion> _otherSuggestions;
 
   bool _hasErrors;
 
-  List<SourceFileEdit> _fixes;
+  List<SourceFileEdit> _edits;
 
   /**
-   * A list of human readable changes made by applying the fixes.
+   * A list of recommended changes that can be automatically made by applying
+   * the 'edits' included in this response.
    */
-  List<String> get descriptionOfFixes => _descriptionOfFixes;
+  List<DartFixSuggestion> get suggestions => _suggestions;
 
   /**
-   * A list of human readable changes made by applying the fixes.
+   * A list of recommended changes that can be automatically made by applying
+   * the 'edits' included in this response.
    */
-  void set descriptionOfFixes(List<String> value) {
+  void set suggestions(List<DartFixSuggestion> value) {
     assert(value != null);
-    this._descriptionOfFixes = value;
+    this._suggestions = value;
   }
 
   /**
-   * A list of human readable recommended changes that cannot be made
-   * automatically.
+   * A list of recommended changes that could not be automatically made.
    */
-  List<String> get otherRecommendations => _otherRecommendations;
+  List<DartFixSuggestion> get otherSuggestions => _otherSuggestions;
 
   /**
-   * A list of human readable recommended changes that cannot be made
-   * automatically.
+   * A list of recommended changes that could not be automatically made.
    */
-  void set otherRecommendations(List<String> value) {
+  void set otherSuggestions(List<DartFixSuggestion> value) {
     assert(value != null);
-    this._otherRecommendations = value;
+    this._otherSuggestions = value;
   }
 
   /**
    * True if the analyzed source contains errors that might impact the
-   * correctness of the recommended fixes that can be automatically applied.
+   * correctness of the recommended changes that can be automatically applied.
    */
   bool get hasErrors => _hasErrors;
 
   /**
    * True if the analyzed source contains errors that might impact the
-   * correctness of the recommended fixes that can be automatically applied.
+   * correctness of the recommended changes that can be automatically applied.
    */
   void set hasErrors(bool value) {
     assert(value != null);
@@ -6329,27 +6428,27 @@
   }
 
   /**
-   * The suggested fixes.
+   * A list of source edits to apply the recommended changes.
    */
-  List<SourceFileEdit> get fixes => _fixes;
+  List<SourceFileEdit> get edits => _edits;
 
   /**
-   * The suggested fixes.
+   * A list of source edits to apply the recommended changes.
    */
-  void set fixes(List<SourceFileEdit> value) {
+  void set edits(List<SourceFileEdit> value) {
     assert(value != null);
-    this._fixes = value;
+    this._edits = value;
   }
 
   EditDartfixResult(
-      List<String> descriptionOfFixes,
-      List<String> otherRecommendations,
+      List<DartFixSuggestion> suggestions,
+      List<DartFixSuggestion> otherSuggestions,
       bool hasErrors,
-      List<SourceFileEdit> fixes) {
-    this.descriptionOfFixes = descriptionOfFixes;
-    this.otherRecommendations = otherRecommendations;
+      List<SourceFileEdit> edits) {
+    this.suggestions = suggestions;
+    this.otherSuggestions = otherSuggestions;
     this.hasErrors = hasErrors;
-    this.fixes = fixes;
+    this.edits = edits;
   }
 
   factory EditDartfixResult.fromJson(
@@ -6358,23 +6457,25 @@
       json = {};
     }
     if (json is Map) {
-      List<String> descriptionOfFixes;
-      if (json.containsKey("descriptionOfFixes")) {
-        descriptionOfFixes = jsonDecoder.decodeList(
-            jsonPath + ".descriptionOfFixes",
-            json["descriptionOfFixes"],
-            jsonDecoder.decodeString);
+      List<DartFixSuggestion> suggestions;
+      if (json.containsKey("suggestions")) {
+        suggestions = jsonDecoder.decodeList(
+            jsonPath + ".suggestions",
+            json["suggestions"],
+            (String jsonPath, Object json) =>
+                new DartFixSuggestion.fromJson(jsonDecoder, jsonPath, json));
       } else {
-        throw jsonDecoder.mismatch(jsonPath, "descriptionOfFixes");
+        throw jsonDecoder.mismatch(jsonPath, "suggestions");
       }
-      List<String> otherRecommendations;
-      if (json.containsKey("otherRecommendations")) {
-        otherRecommendations = jsonDecoder.decodeList(
-            jsonPath + ".otherRecommendations",
-            json["otherRecommendations"],
-            jsonDecoder.decodeString);
+      List<DartFixSuggestion> otherSuggestions;
+      if (json.containsKey("otherSuggestions")) {
+        otherSuggestions = jsonDecoder.decodeList(
+            jsonPath + ".otherSuggestions",
+            json["otherSuggestions"],
+            (String jsonPath, Object json) =>
+                new DartFixSuggestion.fromJson(jsonDecoder, jsonPath, json));
       } else {
-        throw jsonDecoder.mismatch(jsonPath, "otherRecommendations");
+        throw jsonDecoder.mismatch(jsonPath, "otherSuggestions");
       }
       bool hasErrors;
       if (json.containsKey("hasErrors")) {
@@ -6383,18 +6484,18 @@
       } else {
         throw jsonDecoder.mismatch(jsonPath, "hasErrors");
       }
-      List<SourceFileEdit> fixes;
-      if (json.containsKey("fixes")) {
-        fixes = jsonDecoder.decodeList(
-            jsonPath + ".fixes",
-            json["fixes"],
+      List<SourceFileEdit> edits;
+      if (json.containsKey("edits")) {
+        edits = jsonDecoder.decodeList(
+            jsonPath + ".edits",
+            json["edits"],
             (String jsonPath, Object json) =>
                 new SourceFileEdit.fromJson(jsonDecoder, jsonPath, json));
       } else {
-        throw jsonDecoder.mismatch(jsonPath, "fixes");
+        throw jsonDecoder.mismatch(jsonPath, "edits");
       }
       return new EditDartfixResult(
-          descriptionOfFixes, otherRecommendations, hasErrors, fixes);
+          suggestions, otherSuggestions, hasErrors, edits);
     } else {
       throw jsonDecoder.mismatch(jsonPath, "edit.dartfix result", json);
     }
@@ -6410,11 +6511,14 @@
   @override
   Map<String, dynamic> toJson() {
     Map<String, dynamic> result = {};
-    result["descriptionOfFixes"] = descriptionOfFixes;
-    result["otherRecommendations"] = otherRecommendations;
+    result["suggestions"] =
+        suggestions.map((DartFixSuggestion value) => value.toJson()).toList();
+    result["otherSuggestions"] = otherSuggestions
+        .map((DartFixSuggestion value) => value.toJson())
+        .toList();
     result["hasErrors"] = hasErrors;
-    result["fixes"] =
-        fixes.map((SourceFileEdit value) => value.toJson()).toList();
+    result["edits"] =
+        edits.map((SourceFileEdit value) => value.toJson()).toList();
     return result;
   }
 
@@ -6429,12 +6533,12 @@
   @override
   bool operator ==(other) {
     if (other is EditDartfixResult) {
-      return listEqual(descriptionOfFixes, other.descriptionOfFixes,
-              (String a, String b) => a == b) &&
-          listEqual(otherRecommendations, other.otherRecommendations,
-              (String a, String b) => a == b) &&
+      return listEqual(suggestions, other.suggestions,
+              (DartFixSuggestion a, DartFixSuggestion b) => a == b) &&
+          listEqual(otherSuggestions, other.otherSuggestions,
+              (DartFixSuggestion a, DartFixSuggestion b) => a == b) &&
           hasErrors == other.hasErrors &&
-          listEqual(fixes, other.fixes,
+          listEqual(edits, other.edits,
               (SourceFileEdit a, SourceFileEdit b) => a == b);
     }
     return false;
@@ -6443,10 +6547,10 @@
   @override
   int get hashCode {
     int hash = 0;
-    hash = JenkinsSmiHash.combine(hash, descriptionOfFixes.hashCode);
-    hash = JenkinsSmiHash.combine(hash, otherRecommendations.hashCode);
+    hash = JenkinsSmiHash.combine(hash, suggestions.hashCode);
+    hash = JenkinsSmiHash.combine(hash, otherSuggestions.hashCode);
     hash = JenkinsSmiHash.combine(hash, hasErrors.hashCode);
-    hash = JenkinsSmiHash.combine(hash, fixes.hashCode);
+    hash = JenkinsSmiHash.combine(hash, edits.hashCode);
     return JenkinsSmiHash.finish(hash);
   }
 }
diff --git a/pkg/analysis_server/lib/src/edit/edit_dartfix.dart b/pkg/analysis_server/lib/src/edit/edit_dartfix.dart
index 3a8e344..6673fb4 100644
--- a/pkg/analysis_server/lib/src/edit/edit_dartfix.dart
+++ b/pkg/analysis_server/lib/src/edit/edit_dartfix.dart
@@ -23,7 +23,7 @@
 import 'package:analyzer/src/lint/registry.dart';
 import 'package:analyzer/src/services/lint.dart';
 import 'package:analyzer_plugin/protocol/protocol_common.dart'
-    show SourceChange, SourceEdit, SourceFileEdit;
+    show Location, SourceChange, SourceEdit, SourceFileEdit;
 import 'package:front_end/src/fasta/fasta_codes.dart';
 import 'package:front_end/src/scanner/token.dart';
 import 'package:source_span/src/span.dart';
@@ -34,14 +34,14 @@
   final fixFolders = <Folder>[];
   final fixFiles = <File>[];
 
-  List<String> descriptionOfFixes;
-  List<String> otherRecommendations;
+  List<DartFixSuggestion> suggestions;
+  List<DartFixSuggestion> otherSuggestions;
   SourceChange sourceChange;
 
   EditDartFix(this.server, this.request);
 
-  void addFix(String description, SourceChange change) {
-    descriptionOfFixes.add(description);
+  void addFix(String description, Location location, SourceChange change) {
+    suggestions.add(new DartFixSuggestion(description, location: location));
     for (SourceFileEdit fileEdit in change.edits) {
       for (SourceEdit sourceEdit in fileEdit.edits) {
         sourceChange.addEdit(fileEdit.file, fileEdit.fileStamp, sourceEdit);
@@ -49,8 +49,9 @@
     }
   }
 
-  void addRecommendation(String recommendation) {
-    otherRecommendations.add(recommendation);
+  void addRecommendation(String description, [Location location]) {
+    otherSuggestions
+        .add(new DartFixSuggestion(description, location: location));
   }
 
   Future<Response> compute() async {
@@ -125,8 +126,8 @@
     for (String rootPath in contextManager.includedPaths) {
       resources.add(resourceProvider.getResource(rootPath));
     }
-    descriptionOfFixes = <String>[];
-    otherRecommendations = <String>[];
+    suggestions = <DartFixSuggestion>[];
+    otherSuggestions = <DartFixSuggestion>[];
     sourceChange = new SourceChange('dartfix');
     bool hasErrors = false;
     while (resources.isNotEmpty) {
@@ -186,8 +187,8 @@
       await fix.applyRemainingFixes();
     }
 
-    return new EditDartfixResult(descriptionOfFixes, otherRecommendations,
-            hasErrors, sourceChange.edits)
+    return new EditDartfixResult(
+            suggestions, otherSuggestions, hasErrors, sourceChange.edits)
         .toResponse(request.id);
   }
 
@@ -204,7 +205,6 @@
       return false;
     }
 
-    final location = '${locationDescription(result, error.offset)}';
     final dartContext = new DartFixContextImpl(
         new FixContextImpl(
             server.resourceProvider, result.driver, error, result.errors),
@@ -212,12 +212,13 @@
         result.unit);
     final processor = new FixProcessor(dartContext);
     Fix fix = await processor.computeFix();
+    final location = locationFor(result, error.offset, error.length);
     if (fix != null) {
-      addFix('${fix.change.message} in $location', fix.change);
+      addFix(fix.change.message, location, fix.change);
     } else {
       // TODO(danrubel): Determine why the fix could not be applied
       // and report that in the description.
-      addRecommendation('Could not fix "${error.message}" in $location');
+      addRecommendation('Could not fix "${error.message}"', location);
     }
     return true;
   }
@@ -240,29 +241,11 @@
     return false;
   }
 
-  /// Return a human readable description of the specified offset and file.
-  String locationDescription(AnalysisResult result, int offset) {
-    // TODO(danrubel): Pass the location back to the client along with the
-    // message indicating what was or was not automatically fixed
-    // rather than interpreting and integrating the location into the message.
-    final description = new StringBuffer();
-    // Determine the relative path
-    for (Folder folder in fixFolders) {
-      if (folder.contains(result.path)) {
-        description.write(server.resourceProvider.pathContext
-            .relative(result.path, from: folder.path));
-        break;
-      }
-    }
-    if (description.isEmpty) {
-      description.write(result.path);
-    }
-    // Determine the line and column number
-    if (offset >= 0) {
-      final loc = result.unit.lineInfo.getLocation(offset);
-      description.write(':${loc.lineNumber}');
-    }
-    return description.toString();
+  Location locationFor(AnalysisResult result, int offset, int length) {
+    final locInfo = result.unit.lineInfo.getLocation(offset);
+    final location = new Location(
+        result.path, offset, length, locInfo.lineNumber, locInfo.columnNumber);
+    return location;
   }
 }
 
diff --git a/pkg/analysis_server/lib/src/edit/fix/prefer_int_literals_fix.dart b/pkg/analysis_server/lib/src/edit/fix/prefer_int_literals_fix.dart
index 81758f9..52dda1f 100644
--- a/pkg/analysis_server/lib/src/edit/fix/prefer_int_literals_fix.dart
+++ b/pkg/analysis_server/lib/src/edit/fix/prefer_int_literals_fix.dart
@@ -23,18 +23,18 @@
           new EditDartFixAssistContext(dartFix, source, result.unit, literal));
       List<Assist> assists =
           await processor.computeAssist(DartAssistKind.CONVERT_TO_INT_LITERAL);
-      final location = dartFix.locationDescription(result, literal.offset);
+      final location =
+          dartFix.locationFor(result, literal.offset, literal.length);
       if (assists.isNotEmpty) {
         for (Assist assist in assists) {
-          dartFix.addFix(
-              'Replace a double literal with an int literal in $location',
-              assist.change);
+          dartFix.addFix('Replace a double literal with an int literal',
+              location, assist.change);
         }
       } else {
         // TODO(danrubel): If assists is empty, then determine why
         // assist could not be performed and report that in the description.
-        dartFix.addRecommendation('Could not replace'
-            ' a double literal with an int literal in $location');
+        dartFix.addRecommendation(
+            'Could not replace a double literal with an int literal', location);
       }
     }
   }
diff --git a/pkg/analysis_server/lib/src/edit/fix/prefer_mixin_fix.dart b/pkg/analysis_server/lib/src/edit/fix/prefer_mixin_fix.dart
index 5e2aad1..52f924a 100644
--- a/pkg/analysis_server/lib/src/edit/fix/prefer_mixin_fix.dart
+++ b/pkg/analysis_server/lib/src/edit/fix/prefer_mixin_fix.dart
@@ -41,11 +41,11 @@
                 dartFix, elem.source, result.unit, declaration.name));
         List<Assist> assists = await processor
             .computeAssist(DartAssistKind.CONVERT_CLASS_TO_MIXIN);
-        final location = dartFix.locationDescription(result, elem.nameOffset);
+        final location =
+            dartFix.locationFor(result, elem.nameOffset, elem.nameLength);
         if (assists.isNotEmpty) {
           for (Assist assist in assists) {
-            dartFix.addFix(
-                'Convert ${elem.displayName} to a mixin in $location',
+            dartFix.addFix('Convert ${elem.displayName} to a mixin', location,
                 assist.change);
           }
         } else {
@@ -53,7 +53,8 @@
           // assist could not be performed and report that in the description.
           dartFix.addRecommendation(
               'Could not convert ${elem.displayName} to a mixin'
-              ' because the class contains a constructor in $location');
+              ' because the class contains a constructor',
+              location);
         }
       }
     }
diff --git a/pkg/analysis_server/test/integration/support/integration_test_methods.dart b/pkg/analysis_server/test/integration/support/integration_test_methods.dart
index 3976d6c..401723d 100644
--- a/pkg/analysis_server/test/integration/support/integration_test_methods.dart
+++ b/pkg/analysis_server/test/integration/support/integration_test_methods.dart
@@ -1517,23 +1517,24 @@
    *
    * Returns
    *
-   * descriptionOfFixes: List<String>
+   * suggestions: List<DartFixSuggestion>
    *
-   *   A list of human readable changes made by applying the fixes.
+   *   A list of recommended changes that can be automatically made by applying
+   *   the 'edits' included in this response.
    *
-   * otherRecommendations: List<String>
+   * otherSuggestions: List<DartFixSuggestion>
    *
-   *   A list of human readable recommended changes that cannot be made
-   *   automatically.
+   *   A list of recommended changes that could not be automatically made.
    *
    * hasErrors: bool
    *
    *   True if the analyzed source contains errors that might impact the
-   *   correctness of the recommended fixes that can be automatically applied.
+   *   correctness of the recommended changes that can be automatically
+   *   applied.
    *
-   * fixes: List<SourceFileEdit>
+   * edits: List<SourceFileEdit>
    *
-   *   The suggested fixes.
+   *   A list of source edits to apply the recommended changes.
    */
   Future<EditDartfixResult> sendEditDartfix(List<String> included) async {
     var params = new EditDartfixParams(included).toJson();
diff --git a/pkg/analysis_server/test/integration/support/protocol_matchers.dart b/pkg/analysis_server/test/integration/support/protocol_matchers.dart
index db9505f..13ba921 100644
--- a/pkg/analysis_server/test/integration/support/protocol_matchers.dart
+++ b/pkg/analysis_server/test/integration/support/protocol_matchers.dart
@@ -298,6 +298,18 @@
         }));
 
 /**
+ * DartFixSuggestion
+ *
+ * {
+ *   "description": String
+ *   "location": optional Location
+ * }
+ */
+final Matcher isDartFixSuggestion = new LazyMatcher(() => new MatchesJsonObject(
+    "DartFixSuggestion", {"description": isString},
+    optionalFields: {"location": isLocation}));
+
+/**
  * Element
  *
  * {
@@ -2131,18 +2143,18 @@
  * edit.dartfix result
  *
  * {
- *   "descriptionOfFixes": List<String>
- *   "otherRecommendations": List<String>
+ *   "suggestions": List<DartFixSuggestion>
+ *   "otherSuggestions": List<DartFixSuggestion>
  *   "hasErrors": bool
- *   "fixes": List<SourceFileEdit>
+ *   "edits": List<SourceFileEdit>
  * }
  */
 final Matcher isEditDartfixResult =
     new LazyMatcher(() => new MatchesJsonObject("edit.dartfix result", {
-          "descriptionOfFixes": isListOf(isString),
-          "otherRecommendations": isListOf(isString),
+          "suggestions": isListOf(isDartFixSuggestion),
+          "otherSuggestions": isListOf(isDartFixSuggestion),
           "hasErrors": isBool,
-          "fixes": isListOf(isSourceFileEdit)
+          "edits": isListOf(isSourceFileEdit)
         }));
 
 /**
diff --git a/pkg/analysis_server/tool/spec/generated/java/types/DartFixSuggestion.java b/pkg/analysis_server/tool/spec/generated/java/types/DartFixSuggestion.java
new file mode 100644
index 0000000..7a106f8
--- /dev/null
+++ b/pkg/analysis_server/tool/spec/generated/java/types/DartFixSuggestion.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2018, 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.
+ *
+ * This file has been automatically generated. Please do not edit it manually.
+ * To regenerate the file, use the script "pkg/analysis_server/tool/spec/generate_files".
+ */
+package org.dartlang.analysis.server.protocol;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import com.google.common.collect.Lists;
+import com.google.dart.server.utilities.general.JsonUtilities;
+import com.google.dart.server.utilities.general.ObjectUtilities;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import java.util.ArrayList;
+import java.util.Iterator;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * A suggestion from an edit.dartfix request.
+ *
+ * @coverage dart.server.generated.types
+ */
+@SuppressWarnings("unused")
+public class DartFixSuggestion {
+
+  public static final DartFixSuggestion[] EMPTY_ARRAY = new DartFixSuggestion[0];
+
+  public static final List<DartFixSuggestion> EMPTY_LIST = Lists.newArrayList();
+
+  /**
+   * A human readable description of the suggested change.
+   */
+  private final String description;
+
+  /**
+   * The location of the suggested change.
+   */
+  private final Location location;
+
+  /**
+   * Constructor for {@link DartFixSuggestion}.
+   */
+  public DartFixSuggestion(String description, Location location) {
+    this.description = description;
+    this.location = location;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof DartFixSuggestion) {
+      DartFixSuggestion other = (DartFixSuggestion) obj;
+      return
+        ObjectUtilities.equals(other.description, description) &&
+        ObjectUtilities.equals(other.location, location);
+    }
+    return false;
+  }
+
+  public static DartFixSuggestion fromJson(JsonObject jsonObject) {
+    String description = jsonObject.get("description").getAsString();
+    Location location = jsonObject.get("location") == null ? null : Location.fromJson(jsonObject.get("location").getAsJsonObject());
+    return new DartFixSuggestion(description, location);
+  }
+
+  public static List<DartFixSuggestion> fromJsonArray(JsonArray jsonArray) {
+    if (jsonArray == null) {
+      return EMPTY_LIST;
+    }
+    ArrayList<DartFixSuggestion> list = new ArrayList<DartFixSuggestion>(jsonArray.size());
+    Iterator<JsonElement> iterator = jsonArray.iterator();
+    while (iterator.hasNext()) {
+      list.add(fromJson(iterator.next().getAsJsonObject()));
+    }
+    return list;
+  }
+
+  /**
+   * A human readable description of the suggested change.
+   */
+  public String getDescription() {
+    return description;
+  }
+
+  /**
+   * The location of the suggested change.
+   */
+  public Location getLocation() {
+    return location;
+  }
+
+  @Override
+  public int hashCode() {
+    HashCodeBuilder builder = new HashCodeBuilder();
+    builder.append(description);
+    builder.append(location);
+    return builder.toHashCode();
+  }
+
+  public JsonObject toJson() {
+    JsonObject jsonObject = new JsonObject();
+    jsonObject.addProperty("description", description);
+    if (location != null) {
+      jsonObject.add("location", location.toJson());
+    }
+    return jsonObject;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder builder = new StringBuilder();
+    builder.append("[");
+    builder.append("description=");
+    builder.append(description + ", ");
+    builder.append("location=");
+    builder.append(location);
+    builder.append("]");
+    return builder.toString();
+  }
+
+}
diff --git a/pkg/analysis_server/tool/spec/generated/java/types/RuntimeCompletionExpression.java b/pkg/analysis_server/tool/spec/generated/java/types/RuntimeCompletionExpression.java
index eb6b0f0..b2d6b70 100644
--- a/pkg/analysis_server/tool/spec/generated/java/types/RuntimeCompletionExpression.java
+++ b/pkg/analysis_server/tool/spec/generated/java/types/RuntimeCompletionExpression.java
@@ -24,9 +24,9 @@
 import org.apache.commons.lang3.StringUtils;
 
 /**
- * An expression for which we want to know its runtime type. In expressions like `a.b.c.where((e)
- * =&gt; e.^)` we want to know the runtime type of `a.b.c` to enforce it statically at the time
- * when we compute completion suggestions, and get better type for `e`.
+ * An expression for which we want to know its runtime type. In expressions like 'a.b.c.where((e)
+ * =&gt; e.^)' we want to know the runtime type of 'a.b.c' to enforce it statically at the time
+ * when we compute completion suggestions, and get better type for 'e'.
  *
  * @coverage dart.server.generated.types
  */
diff --git a/pkg/analysis_server/tool/spec/spec_input.html b/pkg/analysis_server/tool/spec/spec_input.html
index 8eced61..2f66a93 100644
--- a/pkg/analysis_server/tool/spec/spec_input.html
+++ b/pkg/analysis_server/tool/spec/spec_input.html
@@ -1958,35 +1958,36 @@
       </field>
     </params>
     <result>
-      <field name="descriptionOfFixes">
+      <field name="suggestions">
         <list>
-          <ref>String</ref>
+          <ref>DartFixSuggestion</ref>
         </list>
         <p>
-          A list of human readable changes made by applying the fixes.
+          A list of recommended changes that can be automatically made
+          by applying the 'edits' included in this response.
         </p>
       </field>
-      <field name="otherRecommendations">
+      <field name="otherSuggestions">
         <list>
-          <ref>String</ref>
+          <ref>DartFixSuggestion</ref>
         </list>
         <p>
-          A list of human readable recommended changes that cannot be made automatically.
+          A list of recommended changes that could not be automatically made.
         </p>
       </field>
       <field name="hasErrors">
         <ref>bool</ref>
         <p>
           True if the analyzed source contains errors that might impact the correctness
-          of the recommended fixes that can be automatically applied.
+          of the recommended changes that can be automatically applied.
         </p>
       </field>
-      <field name="fixes">
+      <field name="edits">
         <list>
           <ref>SourceFileEdit</ref>
         </list>
         <p>
-          The suggested fixes.
+          A list of source edits to apply the recommended changes.
         </p>
       </field>
     </result>
@@ -3313,9 +3314,9 @@
   <type name="RuntimeCompletionExpression">
     <p>
       An expression for which we want to know its runtime type.
-      In expressions like `a.b.c.where((e) => e.^)` we want to know the
-      runtime type of `a.b.c` to enforce it statically at the time when we
-      compute completion suggestions, and get better type for `e`.
+      In expressions like 'a.b.c.where((e) => e.^)' we want to know the
+      runtime type of 'a.b.c' to enforce it statically at the time when we
+      compute completion suggestions, and get better type for 'e'.
     </p>
     <object>
       <field name="offset">
@@ -4232,6 +4233,25 @@
       request.
     </p>
   </type>
+  <type name="DartFixSuggestion" experimental="true">
+    <p>
+      A suggestion from an edit.dartfix request.
+    </p>
+    <object>
+      <field name="description">
+        <ref>String</ref>
+        <p>
+          A human readable description of the suggested change.
+        </p>
+      </field>
+      <field name="location" optional="true">
+        <ref>Location</ref>
+        <p>
+          The location of the suggested change.
+        </p>
+      </field>
+    </object>
+  </type>
   <type name="SearchResult">
     <p>
       A single result from a search request.
diff --git a/pkg/analysis_server_client/lib/src/protocol/protocol_constants.dart b/pkg/analysis_server_client/lib/src/protocol/protocol_constants.dart
index 9d85d08..1280460 100644
--- a/pkg/analysis_server_client/lib/src/protocol/protocol_constants.dart
+++ b/pkg/analysis_server_client/lib/src/protocol/protocol_constants.dart
@@ -173,11 +173,10 @@
 const String EDIT_REQUEST_ORGANIZE_DIRECTIVES_FILE = 'file';
 const String EDIT_REQUEST_SORT_MEMBERS = 'edit.sortMembers';
 const String EDIT_REQUEST_SORT_MEMBERS_FILE = 'file';
-const String EDIT_RESPONSE_DARTFIX_DESCRIPTION_OF_FIXES = 'descriptionOfFixes';
-const String EDIT_RESPONSE_DARTFIX_FIXES = 'fixes';
+const String EDIT_RESPONSE_DARTFIX_EDITS = 'edits';
 const String EDIT_RESPONSE_DARTFIX_HAS_ERRORS = 'hasErrors';
-const String EDIT_RESPONSE_DARTFIX_OTHER_RECOMMENDATIONS =
-    'otherRecommendations';
+const String EDIT_RESPONSE_DARTFIX_OTHER_SUGGESTIONS = 'otherSuggestions';
+const String EDIT_RESPONSE_DARTFIX_SUGGESTIONS = 'suggestions';
 const String EDIT_RESPONSE_FORMAT_EDITS = 'edits';
 const String EDIT_RESPONSE_FORMAT_SELECTION_LENGTH = 'selectionLength';
 const String EDIT_RESPONSE_FORMAT_SELECTION_OFFSET = 'selectionOffset';
diff --git a/pkg/analysis_server_client/lib/src/protocol/protocol_generated.dart b/pkg/analysis_server_client/lib/src/protocol/protocol_generated.dart
index 2bbf169..9fabb4c 100644
--- a/pkg/analysis_server_client/lib/src/protocol/protocol_generated.dart
+++ b/pkg/analysis_server_client/lib/src/protocol/protocol_generated.dart
@@ -5932,6 +5932,105 @@
 }
 
 /**
+ * DartFixSuggestion
+ *
+ * {
+ *   "description": String
+ *   "location": optional Location
+ * }
+ *
+ * Clients may not extend, implement or mix-in this class.
+ */
+class DartFixSuggestion implements HasToJson {
+  String _description;
+
+  Location _location;
+
+  /**
+   * A human readable description of the suggested change.
+   */
+  String get description => _description;
+
+  /**
+   * A human readable description of the suggested change.
+   */
+  void set description(String value) {
+    assert(value != null);
+    this._description = value;
+  }
+
+  /**
+   * The location of the suggested change.
+   */
+  Location get location => _location;
+
+  /**
+   * The location of the suggested change.
+   */
+  void set location(Location value) {
+    this._location = value;
+  }
+
+  DartFixSuggestion(String description, {Location location}) {
+    this.description = description;
+    this.location = location;
+  }
+
+  factory DartFixSuggestion.fromJson(
+      JsonDecoder jsonDecoder, String jsonPath, Object json) {
+    if (json == null) {
+      json = {};
+    }
+    if (json is Map) {
+      String description;
+      if (json.containsKey("description")) {
+        description = jsonDecoder.decodeString(
+            jsonPath + ".description", json["description"]);
+      } else {
+        throw jsonDecoder.mismatch(jsonPath, "description");
+      }
+      Location location;
+      if (json.containsKey("location")) {
+        location = new Location.fromJson(
+            jsonDecoder, jsonPath + ".location", json["location"]);
+      }
+      return new DartFixSuggestion(description, location: location);
+    } else {
+      throw jsonDecoder.mismatch(jsonPath, "DartFixSuggestion", json);
+    }
+  }
+
+  @override
+  Map<String, dynamic> toJson() {
+    Map<String, dynamic> result = {};
+    result["description"] = description;
+    if (location != null) {
+      result["location"] = location.toJson();
+    }
+    return result;
+  }
+
+  @override
+  String toString() => json.encode(toJson());
+
+  @override
+  bool operator ==(other) {
+    if (other is DartFixSuggestion) {
+      return description == other.description && location == other.location;
+    }
+    return false;
+  }
+
+  @override
+  int get hashCode {
+    int hash = 0;
+    hash = JenkinsSmiHash.combine(hash, description.hashCode);
+    hash = JenkinsSmiHash.combine(hash, location.hashCode);
+    return JenkinsSmiHash.finish(hash);
+  }
+}
+
+/**
  * diagnostic.getDiagnostics params
  *
  * Clients may not extend, implement or mix-in this class.
@@ -6268,60 +6367,60 @@
  * edit.dartfix result
  *
  * {
- *   "descriptionOfFixes": List<String>
- *   "otherRecommendations": List<String>
+ *   "suggestions": List<DartFixSuggestion>
+ *   "otherSuggestions": List<DartFixSuggestion>
  *   "hasErrors": bool
- *   "fixes": List<SourceFileEdit>
+ *   "edits": List<SourceFileEdit>
  * }
  *
  * Clients may not extend, implement or mix-in this class.
  */
 class EditDartfixResult implements ResponseResult {
-  List<String> _descriptionOfFixes;
+  List<DartFixSuggestion> _suggestions;
 
-  List<String> _otherRecommendations;
+  List<DartFixSuggestion> _otherSuggestions;
 
   bool _hasErrors;
 
-  List<SourceFileEdit> _fixes;
+  List<SourceFileEdit> _edits;
 
   /**
-   * A list of human readable changes made by applying the fixes.
+   * A list of recommended changes that can be automatically made by applying
+   * the 'edits' included in this response.
    */
-  List<String> get descriptionOfFixes => _descriptionOfFixes;
+  List<DartFixSuggestion> get suggestions => _suggestions;
 
   /**
-   * A list of human readable changes made by applying the fixes.
+   * A list of recommended changes that can be automatically made by applying
+   * the 'edits' included in this response.
    */
-  void set descriptionOfFixes(List<String> value) {
+  void set suggestions(List<DartFixSuggestion> value) {
     assert(value != null);
-    this._descriptionOfFixes = value;
+    this._suggestions = value;
   }
 
   /**
-   * A list of human readable recommended changes that cannot be made
-   * automatically.
+   * A list of recommended changes that could not be automatically made.
    */
-  List<String> get otherRecommendations => _otherRecommendations;
+  List<DartFixSuggestion> get otherSuggestions => _otherSuggestions;
 
   /**
-   * A list of human readable recommended changes that cannot be made
-   * automatically.
+   * A list of recommended changes that could not be automatically made.
    */
-  void set otherRecommendations(List<String> value) {
+  void set otherSuggestions(List<DartFixSuggestion> value) {
     assert(value != null);
-    this._otherRecommendations = value;
+    this._otherSuggestions = value;
   }
 
   /**
    * True if the analyzed source contains errors that might impact the
-   * correctness of the recommended fixes that can be automatically applied.
+   * correctness of the recommended changes that can be automatically applied.
    */
   bool get hasErrors => _hasErrors;
 
   /**
    * True if the analyzed source contains errors that might impact the
-   * correctness of the recommended fixes that can be automatically applied.
+   * correctness of the recommended changes that can be automatically applied.
    */
   void set hasErrors(bool value) {
     assert(value != null);
@@ -6329,27 +6428,27 @@
   }
 
   /**
-   * The suggested fixes.
+   * A list of source edits to apply the recommended changes.
    */
-  List<SourceFileEdit> get fixes => _fixes;
+  List<SourceFileEdit> get edits => _edits;
 
   /**
-   * The suggested fixes.
+   * A list of source edits to apply the recommended changes.
    */
-  void set fixes(List<SourceFileEdit> value) {
+  void set edits(List<SourceFileEdit> value) {
     assert(value != null);
-    this._fixes = value;
+    this._edits = value;
   }
 
   EditDartfixResult(
-      List<String> descriptionOfFixes,
-      List<String> otherRecommendations,
+      List<DartFixSuggestion> suggestions,
+      List<DartFixSuggestion> otherSuggestions,
       bool hasErrors,
-      List<SourceFileEdit> fixes) {
-    this.descriptionOfFixes = descriptionOfFixes;
-    this.otherRecommendations = otherRecommendations;
+      List<SourceFileEdit> edits) {
+    this.suggestions = suggestions;
+    this.otherSuggestions = otherSuggestions;
     this.hasErrors = hasErrors;
-    this.fixes = fixes;
+    this.edits = edits;
   }
 
   factory EditDartfixResult.fromJson(
@@ -6358,23 +6457,25 @@
       json = {};
     }
     if (json is Map) {
-      List<String> descriptionOfFixes;
-      if (json.containsKey("descriptionOfFixes")) {
-        descriptionOfFixes = jsonDecoder.decodeList(
-            jsonPath + ".descriptionOfFixes",
-            json["descriptionOfFixes"],
-            jsonDecoder.decodeString);
+      List<DartFixSuggestion> suggestions;
+      if (json.containsKey("suggestions")) {
+        suggestions = jsonDecoder.decodeList(
+            jsonPath + ".suggestions",
+            json["suggestions"],
+            (String jsonPath, Object json) =>
+                new DartFixSuggestion.fromJson(jsonDecoder, jsonPath, json));
       } else {
-        throw jsonDecoder.mismatch(jsonPath, "descriptionOfFixes");
+        throw jsonDecoder.mismatch(jsonPath, "suggestions");
       }
-      List<String> otherRecommendations;
-      if (json.containsKey("otherRecommendations")) {
-        otherRecommendations = jsonDecoder.decodeList(
-            jsonPath + ".otherRecommendations",
-            json["otherRecommendations"],
-            jsonDecoder.decodeString);
+      List<DartFixSuggestion> otherSuggestions;
+      if (json.containsKey("otherSuggestions")) {
+        otherSuggestions = jsonDecoder.decodeList(
+            jsonPath + ".otherSuggestions",
+            json["otherSuggestions"],
+            (String jsonPath, Object json) =>
+                new DartFixSuggestion.fromJson(jsonDecoder, jsonPath, json));
       } else {
-        throw jsonDecoder.mismatch(jsonPath, "otherRecommendations");
+        throw jsonDecoder.mismatch(jsonPath, "otherSuggestions");
       }
       bool hasErrors;
       if (json.containsKey("hasErrors")) {
@@ -6383,18 +6484,18 @@
       } else {
         throw jsonDecoder.mismatch(jsonPath, "hasErrors");
       }
-      List<SourceFileEdit> fixes;
-      if (json.containsKey("fixes")) {
-        fixes = jsonDecoder.decodeList(
-            jsonPath + ".fixes",
-            json["fixes"],
+      List<SourceFileEdit> edits;
+      if (json.containsKey("edits")) {
+        edits = jsonDecoder.decodeList(
+            jsonPath + ".edits",
+            json["edits"],
             (String jsonPath, Object json) =>
                 new SourceFileEdit.fromJson(jsonDecoder, jsonPath, json));
       } else {
-        throw jsonDecoder.mismatch(jsonPath, "fixes");
+        throw jsonDecoder.mismatch(jsonPath, "edits");
       }
       return new EditDartfixResult(
-          descriptionOfFixes, otherRecommendations, hasErrors, fixes);
+          suggestions, otherSuggestions, hasErrors, edits);
     } else {
       throw jsonDecoder.mismatch(jsonPath, "edit.dartfix result", json);
     }
@@ -6410,11 +6511,14 @@
   @override
   Map<String, dynamic> toJson() {
     Map<String, dynamic> result = {};
-    result["descriptionOfFixes"] = descriptionOfFixes;
-    result["otherRecommendations"] = otherRecommendations;
+    result["suggestions"] =
+        suggestions.map((DartFixSuggestion value) => value.toJson()).toList();
+    result["otherSuggestions"] = otherSuggestions
+        .map((DartFixSuggestion value) => value.toJson())
+        .toList();
     result["hasErrors"] = hasErrors;
-    result["fixes"] =
-        fixes.map((SourceFileEdit value) => value.toJson()).toList();
+    result["edits"] =
+        edits.map((SourceFileEdit value) => value.toJson()).toList();
     return result;
   }
 
@@ -6429,12 +6533,12 @@
   @override
   bool operator ==(other) {
     if (other is EditDartfixResult) {
-      return listEqual(descriptionOfFixes, other.descriptionOfFixes,
-              (String a, String b) => a == b) &&
-          listEqual(otherRecommendations, other.otherRecommendations,
-              (String a, String b) => a == b) &&
+      return listEqual(suggestions, other.suggestions,
+              (DartFixSuggestion a, DartFixSuggestion b) => a == b) &&
+          listEqual(otherSuggestions, other.otherSuggestions,
+              (DartFixSuggestion a, DartFixSuggestion b) => a == b) &&
           hasErrors == other.hasErrors &&
-          listEqual(fixes, other.fixes,
+          listEqual(edits, other.edits,
               (SourceFileEdit a, SourceFileEdit b) => a == b);
     }
     return false;
@@ -6443,10 +6547,10 @@
   @override
   int get hashCode {
     int hash = 0;
-    hash = JenkinsSmiHash.combine(hash, descriptionOfFixes.hashCode);
-    hash = JenkinsSmiHash.combine(hash, otherRecommendations.hashCode);
+    hash = JenkinsSmiHash.combine(hash, suggestions.hashCode);
+    hash = JenkinsSmiHash.combine(hash, otherSuggestions.hashCode);
     hash = JenkinsSmiHash.combine(hash, hasErrors.hashCode);
-    hash = JenkinsSmiHash.combine(hash, fixes.hashCode);
+    hash = JenkinsSmiHash.combine(hash, edits.hashCode);
     return JenkinsSmiHash.finish(hash);
   }
 }
diff --git a/pkg/dartfix/lib/src/driver.dart b/pkg/dartfix/lib/src/driver.dart
index 80b6c11..80e7a05 100644
--- a/pkg/dartfix/lib/src/driver.dart
+++ b/pkg/dartfix/lib/src/driver.dart
@@ -122,25 +122,23 @@
   }
 
   Future applyFixes(EditDartfixResult result) async {
-    showDescriptions('Recommended changes', result.descriptionOfFixes);
-    showDescriptions(
-      'Recommended changes that cannot not be automatically applied',
-      result.otherRecommendations,
-    );
-    if (result.descriptionOfFixes.isEmpty) {
+    showDescriptions('Recommended changes', result.suggestions);
+    showDescriptions('Recommended changes that cannot be automatically applied',
+        result.otherSuggestions);
+    if (result.suggestions.isEmpty) {
       logger.stdout('');
-      logger.stdout(result.otherRecommendations.isNotEmpty
-          ? 'No recommended changes that cannot be automatically applied.'
+      logger.stdout(result.otherSuggestions.isNotEmpty
+          ? 'None of the recommended changes can be automatically applied.'
           : 'No recommended changes.');
       return;
     }
     logger.stdout('');
     logger.stdout(ansi.emphasized('Files to be changed:'));
-    for (SourceFileEdit fileEdit in result.fixes) {
+    for (SourceFileEdit fileEdit in result.edits) {
       logger.stdout('  ${_relativePath(fileEdit.file)}');
     }
     if (shouldApplyChanges(result)) {
-      for (SourceFileEdit fileEdit in result.fixes) {
+      for (SourceFileEdit fileEdit in result.edits) {
         final file = new File(fileEdit.file);
         String code = await file.readAsString();
         for (SourceEdit edit in fileEdit.edits) {
@@ -152,13 +150,16 @@
     }
   }
 
-  void showDescriptions(String title, List<String> descriptions) {
-    if (descriptions.isNotEmpty) {
+  void showDescriptions(String title, List<DartFixSuggestion> suggestions) {
+    if (suggestions.isNotEmpty) {
       logger.stdout('');
       logger.stdout(ansi.emphasized('$title:'));
-      List<String> sorted = new List.from(descriptions)..sort();
-      for (String line in sorted) {
-        logger.stdout('  $line');
+      List<DartFixSuggestion> sorted = new List.from(suggestions)
+        ..sort(compareSuggestions);
+      for (DartFixSuggestion suggestion in sorted) {
+        Location loc = suggestion.location;
+        logger.stdout('  ${_toSentenceFragment(suggestion.description)}'
+            '${loc == null ? "" : " • ${loc.startLine}:${loc.startColumn}"}');
       }
     }
   }
@@ -327,6 +328,14 @@
     }
   }
 
+  int compareSuggestions(DartFixSuggestion s1, DartFixSuggestion s2) {
+    int result = s1.description.compareTo(s2.description);
+    if (result != 0) {
+      return result;
+    }
+    return (s2.location?.offset ?? 0) - (s1.location?.offset ?? 0);
+  }
+
   bool shouldFilterError(AnalysisError error) {
     // Do not show TODOs or errors that will be automatically fixed.