Version 2.17.0-164.0.dev
Merge commit 'f5bd632abe415e32784e619971d84c9562ed0fd3' into 'dev'
diff --git a/pkg/analysis_server/lib/src/lsp/mapping.dart b/pkg/analysis_server/lib/src/lsp/mapping.dart
index fec47e8..9026e05 100644
--- a/pkg/analysis_server/lib/src/lsp/mapping.dart
+++ b/pkg/analysis_server/lib/src/lsp/mapping.dart
@@ -983,10 +983,12 @@
);
// For LSP, we need to provide the main edit and other edits separately. The
- // main edit must include the location that completion was invoked. If we
- // fail to find exactly one, this is an error.
+ // main edit must include the location that completion was invoked. If we find
+ // more than one, take the first one since imports are usually added as later
+ // edits (so when applied sequentially they will be inserted at the start of
+ // the file after the other edits).
final mainEdit = mainFileEdits
- .singleWhere((edit) => edit.range.start.line == position.line);
+ .firstWhere((edit) => edit.range.start.line == position.line);
final nonMainEdits = mainFileEdits.where((edit) => edit != mainEdit).toList();
return lsp.CompletionItem(
@@ -1481,12 +1483,16 @@
}) {
final snippetEdits = <lsp.SnippetTextEdit>[];
- // Edit groups offsets are based on after the edits are applied, so we
- // must track the offset delta to ensure we track the offset within the
- // edits. This also requires the edits are sorted earliest-to-latest.
+ // Edit groups offsets are based on the document after the edits are applied.
+ // This means we must compute an offset delta for each edit that takes into
+ // account all edits that might be made before it in the document (which are
+ // after it in the edits). To do this, reverse the list when computing the
+ // offsets, but reverse them back to the original list order when returning so
+ // that we do not apply them incorrectly in tests (where we will apply them
+ // in-sequence).
+
var offsetDelta = 0;
- change.edits.sortBy<num>((edit) => edit.offset);
- for (final edit in change.edits) {
+ for (final edit in change.edits.reversed) {
snippetEdits.add(snippetTextEditFromEditGroups(
filePath,
lineInfo,
@@ -1499,7 +1505,7 @@
offsetDelta += edit.replacement.length - edit.length;
}
- return snippetEdits;
+ return snippetEdits.reversed.toList();
}
ErrorOr<server.SourceRange> toSourceRange(
diff --git a/pkg/analysis_server/test/lsp/completion_dart_test.dart b/pkg/analysis_server/test/lsp/completion_dart_test.dart
index 0db91bd..1b9520b 100644
--- a/pkg/analysis_server/test/lsp/completion_dart_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_dart_test.dart
@@ -2607,6 +2607,60 @@
''');
}
+ Future<void> test_snippets_flutterStateless_addsImports_onlyPrefix() async {
+ final content = '''
+stless^
+''';
+
+ await initializeWithSnippetSupportAndPreviewFlag();
+ final updated = await expectAndApplySnippet(
+ content,
+ prefix: FlutterStatelessWidgetSnippetProducer.prefix,
+ label: FlutterStatelessWidgetSnippetProducer.label,
+ );
+
+ expect(updated, '''
+import 'package:flutter/src/foundation/key.dart';
+import 'package:flutter/src/widgets/framework.dart';
+
+class \${1:MyWidget} extends StatelessWidget {
+ const \${1:MyWidget}({Key$questionSuffix key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ \$0
+ }
+}
+''');
+ }
+
+ Future<void> test_snippets_flutterStateless_addsImports_zeroOffset() async {
+ final content = '''
+^
+'''; // Deliberate trailing newline to ensure imports aren't inserted at "end".
+
+ await initializeWithSnippetSupportAndPreviewFlag();
+ final updated = await expectAndApplySnippet(
+ content,
+ prefix: FlutterStatelessWidgetSnippetProducer.prefix,
+ label: FlutterStatelessWidgetSnippetProducer.label,
+ );
+
+ expect(updated, '''
+import 'package:flutter/src/foundation/key.dart';
+import 'package:flutter/src/widgets/framework.dart';
+
+class \${1:MyWidget} extends StatelessWidget {
+ const \${1:MyWidget}({Key$questionSuffix key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ \$0
+ }
+}
+''');
+ }
+
Future<void> test_snippets_flutterStateless_notAvailable_notTopLevel() async {
final content = '''
class A {
diff --git a/pkg/analysis_server/test/services/snippets/dart/flutter_snippet_producers_test.dart b/pkg/analysis_server/test/services/snippets/dart/flutter_snippet_producers_test.dart
index ce15a90..6edf13b 100644
--- a/pkg/analysis_server/test/services/snippets/dart/flutter_snippet_producers_test.dart
+++ b/pkg/analysis_server/test/services/snippets/dart/flutter_snippet_producers_test.dart
@@ -2,6 +2,7 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
+import 'package:analysis_server/src/protocol_server.dart';
import 'package:analysis_server/src/services/snippets/dart/flutter_snippet_producers.dart';
import 'package:analysis_server/src/services/snippets/dart/snippet_manager.dart';
import 'package:test/test.dart';
@@ -20,8 +21,14 @@
}
abstract class FlutterSnippetProducerTest extends AbstractSingleUnitTest {
+ SnippetProducerGenerator get generator;
+ String get label;
+ String get prefix;
+
+ @override
+ bool get verifyNoTestUnitErrors => false;
+
Future<void> expectNotValidSnippet(
- SnippetProducerGenerator generator,
String code,
) async {
await resolveTestCode(withoutMarkers(code));
@@ -34,8 +41,7 @@
expect(await producer.isValid(), isFalse);
}
- Future<Snippet> expectValidSnippet(
- SnippetProducerGenerator generator, String code) async {
+ Future<Snippet> expectValidSnippet(String code) async {
await resolveTestCode(withoutMarkers(code));
final request = DartSnippetRequest(
unit: testAnalysisResult,
@@ -46,221 +52,251 @@
expect(await producer.isValid(), isTrue);
return producer.compute();
}
+
+ /// Checks snippets can produce edits where the imports and snippet will be
+ /// inserted at the same location.
+ ///
+ /// For example, when a document is completely empty besides the snippet
+ /// prefix, the imports will be inserted at offset 0 and the snippet will
+ /// replace from 0 to the end of the typed prefix.
+ Future<void> test_valid_importsAndEditsOverlap() async {
+ writeTestPackageConfig(flutter: true);
+
+ final snippet = await expectValidSnippet('$prefix^');
+ expect(snippet.prefix, prefix);
+
+ // Main edits replace $prefix.length characters starting at $prefix
+ final mainEdit = snippet.change.edits[0].edits[0];
+ expect(mainEdit.offset, testCode.indexOf(prefix));
+ expect(mainEdit.length, prefix.length);
+
+ // Imports inserted at start of doc (0)
+ final importEdit = snippet.change.edits[0].edits[1];
+ expect(importEdit.offset, 0);
+ expect(importEdit.length, 0);
+ }
+
+ Future<void> test_valid_suffixReplacement() async {
+ writeTestPackageConfig(flutter: true);
+
+ final snippet = await expectValidSnippet('''
+class A {}
+
+$prefix^
+''');
+ expect(snippet.prefix, prefix);
+
+ // Main edits replace $prefix.length characters starting at $prefix
+ final mainEdit = snippet.change.edits[0].edits[0];
+ expect(mainEdit.offset, testCode.indexOf(prefix));
+ expect(mainEdit.length, prefix.length);
+
+ // Imports inserted at start of doc (0)
+ final importEdit = snippet.change.edits[0].edits[1];
+ expect(importEdit.offset, 0);
+ expect(importEdit.length, 0);
+ }
}
@reflectiveTest
class FlutterStatefulWidgetSnippetProducerTest
extends FlutterSnippetProducerTest {
+ @override
final generator = FlutterStatefulWidgetSnippetProducer.newInstance;
+
+ @override
+ String get label => FlutterStatefulWidgetSnippetProducer.label;
+
+ @override
+ String get prefix => FlutterStatefulWidgetSnippetProducer.prefix;
+
Future<void> test_notValid_notFlutterProject() async {
writeTestPackageConfig();
- await expectNotValidSnippet(generator, '^');
+ await expectNotValidSnippet('^');
}
Future<void> test_valid() async {
writeTestPackageConfig(flutter: true);
- final snippet = await expectValidSnippet(generator, '^');
+ final snippet = await expectValidSnippet('^');
expect(snippet.prefix, 'stful');
expect(snippet.label, 'Flutter Stateful Widget');
- expect(snippet.change.toJson(), {
- 'message': '',
- 'edits': [
- {
- 'file': testFile,
- 'fileStamp': 0,
- 'edits': [
- {
- 'offset': 0,
- 'length': 0,
- 'replacement':
- 'import \'package:flutter/src/foundation/key.dart\';\n'
- 'import \'package:flutter/src/widgets/framework.dart\';\n'
- },
- {
- 'offset': 0,
- 'length': 0,
- 'replacement': 'class MyWidget extends StatefulWidget {\n'
- ' const MyWidget({Key? key}) : super(key: key);\n'
- '\n'
- ' @override\n'
- ' State<MyWidget> createState() => _MyWidgetState();\n'
- '}\n'
- '\n'
- 'class _MyWidgetState extends State<MyWidget> {\n'
- ' @override\n'
- ' Widget build(BuildContext context) {\n'
- ' \n'
- ' }\n'
- '}'
- }
- ]
- }
- ],
- 'linkedEditGroups': [
- {
- 'positions': [
- {'file': testFile, 'offset': 109},
- {'file': testFile, 'offset': 151},
- {'file': testFile, 'offset': 212},
- {'file': testFile, 'offset': 240},
- {'file': testFile, 'offset': 267},
- {'file': testFile, 'offset': 295}
- ],
- 'length': 8,
- 'suggestions': []
- }
- ],
- 'selection': {'file': testFile, 'offset': 362}
- });
+ var code = '';
+ expect(snippet.change.edits, hasLength(1));
+ snippet.change.edits
+ .forEach((edit) => code = SourceEdit.applySequence(code, edit.edits));
+ expect(code, '''
+import 'package:flutter/src/foundation/key.dart';
+import 'package:flutter/src/widgets/framework.dart';
+
+class MyWidget extends StatefulWidget {
+ const MyWidget({Key? key}) : super(key: key);
+
+ @override
+ State<MyWidget> createState() => _MyWidgetState();
+}
+
+class _MyWidgetState extends State<MyWidget> {
+ @override
+ Widget build(BuildContext context) {
+
+ }
+}''');
+ expect(snippet.change.selection!.file, testFile);
+ expect(snippet.change.selection!.offset, 363);
+ expect(snippet.change.linkedEditGroups.map((group) => group.toJson()), [
+ {
+ 'positions': [
+ {'file': testFile, 'offset': 110},
+ {'file': testFile, 'offset': 152},
+ {'file': testFile, 'offset': 213},
+ {'file': testFile, 'offset': 241},
+ {'file': testFile, 'offset': 268},
+ {'file': testFile, 'offset': 296},
+ ],
+ 'length': 8,
+ 'suggestions': []
+ }
+ ]);
}
}
@reflectiveTest
class FlutterStatefulWidgetWithAnimationControllerSnippetProducerTest
extends FlutterSnippetProducerTest {
+ @override
final generator =
FlutterStatefulWidgetWithAnimationControllerSnippetProducer.newInstance;
+
+ @override
+ String get label =>
+ FlutterStatefulWidgetWithAnimationControllerSnippetProducer.label;
+
+ @override
+ String get prefix =>
+ FlutterStatefulWidgetWithAnimationControllerSnippetProducer.prefix;
+
Future<void> test_notValid_notFlutterProject() async {
writeTestPackageConfig();
- await expectNotValidSnippet(generator, '^');
+ await expectNotValidSnippet('^');
}
Future<void> test_valid() async {
writeTestPackageConfig(flutter: true);
- final snippet = await expectValidSnippet(generator, '^');
+ final snippet = await expectValidSnippet('^');
expect(snippet.prefix, 'stanim');
expect(snippet.label, 'Flutter Widget with AnimationController');
- expect(snippet.change.toJson(), {
- 'message': '',
- 'edits': [
- {
- 'file': testFile,
- 'fileStamp': 0,
- 'edits': [
- {
- 'offset': 0,
- 'length': 0,
- 'replacement':
- 'import \'package:flutter/src/animation/animation_controller.dart\';\n'
- 'import \'package:flutter/src/foundation/key.dart\';\n'
- 'import \'package:flutter/src/widgets/framework.dart\';\n'
- 'import \'package:flutter/src/widgets/ticker_provider.dart\';\n'
- },
- {
- 'offset': 0,
- 'length': 0,
- 'replacement': 'class MyWidget extends StatefulWidget {\n'
- ' const MyWidget({Key? key}) : super(key: key);\n'
- '\n'
- ' @override\n'
- ' State<MyWidget> createState() => _MyWidgetState();\n'
- '}\n'
- '\n'
- 'class _MyWidgetState extends State<MyWidget>\n'
- ' with SingleTickerProviderStateMixin {\n'
- ' late AnimationController _controller;\n'
- '\n'
- ' @override\n'
- ' void initState() {\n'
- ' super.initState();\n'
- ' _controller = AnimationController(vsync: this);\n'
- ' }\n'
- '\n'
- ' @override\n'
- ' void dispose() {\n'
- ' super.dispose();\n'
- ' _controller.dispose();\n'
- ' }\n'
- '\n'
- ' @override\n'
- ' Widget build(BuildContext context) {\n'
- ' \n'
- ' }\n'
- '}'
- }
- ]
- }
- ],
- 'linkedEditGroups': [
- {
- 'positions': [
- {'file': testFile, 'offset': 234},
- {'file': testFile, 'offset': 276},
- {'file': testFile, 'offset': 337},
- {'file': testFile, 'offset': 365},
- {'file': testFile, 'offset': 392},
- {'file': testFile, 'offset': 420}
- ],
- 'length': 8,
- 'suggestions': []
- }
- ],
- 'selection': {'file': testFile, 'offset': 765}
- });
+ var code = '';
+ expect(snippet.change.edits, hasLength(1));
+ snippet.change.edits
+ .forEach((edit) => code = SourceEdit.applySequence(code, edit.edits));
+ expect(code, '''
+import 'package:flutter/src/animation/animation_controller.dart';
+import 'package:flutter/src/foundation/key.dart';
+import 'package:flutter/src/widgets/framework.dart';
+import 'package:flutter/src/widgets/ticker_provider.dart';
+
+class MyWidget extends StatefulWidget {
+ const MyWidget({Key? key}) : super(key: key);
+
+ @override
+ State<MyWidget> createState() => _MyWidgetState();
+}
+
+class _MyWidgetState extends State<MyWidget>
+ with SingleTickerProviderStateMixin {
+ late AnimationController _controller;
+
+ @override
+ void initState() {
+ super.initState();
+ _controller = AnimationController(vsync: this);
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ _controller.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+
+ }
+}''');
+ expect(snippet.change.selection!.file, testFile);
+ expect(snippet.change.selection!.offset, 766);
+ expect(snippet.change.linkedEditGroups.map((group) => group.toJson()), [
+ {
+ 'positions': [
+ {'file': testFile, 'offset': 235},
+ {'file': testFile, 'offset': 277},
+ {'file': testFile, 'offset': 338},
+ {'file': testFile, 'offset': 366},
+ {'file': testFile, 'offset': 393},
+ {'file': testFile, 'offset': 421},
+ ],
+ 'length': 8,
+ 'suggestions': []
+ }
+ ]);
}
}
@reflectiveTest
class FlutterStatelessWidgetSnippetProducerTest
extends FlutterSnippetProducerTest {
+ @override
final generator = FlutterStatelessWidgetSnippetProducer.newInstance;
+ @override
+ String get label => FlutterStatelessWidgetSnippetProducer.label;
+
+ @override
+ String get prefix => FlutterStatelessWidgetSnippetProducer.prefix;
+
Future<void> test_notValid_notFlutterProject() async {
writeTestPackageConfig();
- await expectNotValidSnippet(generator, '^');
+ await expectNotValidSnippet('^');
}
Future<void> test_valid() async {
writeTestPackageConfig(flutter: true);
- final snippet = await expectValidSnippet(generator, '^');
+ final snippet = await expectValidSnippet('^');
expect(snippet.prefix, 'stless');
expect(snippet.label, 'Flutter Stateless Widget');
- expect(snippet.change.toJson(), {
- 'message': '',
- 'edits': [
- {
- 'file': testFile,
- 'fileStamp': 0,
- 'edits': [
- {
- 'offset': 0,
- 'length': 0,
- 'replacement':
- 'import \'package:flutter/src/foundation/key.dart\';\n'
- 'import \'package:flutter/src/widgets/framework.dart\';\n'
- },
- {
- 'offset': 0,
- 'length': 0,
- 'replacement': 'class MyWidget extends StatelessWidget {\n'
- ' const MyWidget({Key? key}) : super(key: key);\n'
- '\n'
- ' @override\n'
- ' Widget build(BuildContext context) {\n'
- ' \n'
- ' }\n'
- '}'
- }
- ]
- }
- ],
- 'linkedEditGroups': [
- {
- 'positions': [
- {'file': testFile, 'offset': 109},
- {'file': testFile, 'offset': 152}
- ],
- 'length': 8,
- 'suggestions': []
- }
- ],
- 'selection': {'file': testFile, 'offset': 248}
- });
+ var code = '';
+ expect(snippet.change.edits, hasLength(1));
+ snippet.change.edits
+ .forEach((edit) => code = SourceEdit.applySequence(code, edit.edits));
+ expect(code, '''
+import 'package:flutter/src/foundation/key.dart';
+import 'package:flutter/src/widgets/framework.dart';
+
+class MyWidget extends StatelessWidget {
+ const MyWidget({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+
+ }
+}''');
+ expect(snippet.change.selection!.file, testFile);
+ expect(snippet.change.selection!.offset, 249);
+ expect(snippet.change.linkedEditGroups.map((group) => group.toJson()), [
+ {
+ 'positions': [
+ {'file': testFile, 'offset': 110},
+ {'file': testFile, 'offset': 153},
+ ],
+ 'length': 8,
+ 'suggestions': []
+ }
+ ]);
}
}
diff --git a/pkg/analysis_server_client/lib/src/protocol/protocol_common.dart b/pkg/analysis_server_client/lib/src/protocol/protocol_common.dart
index ca03c4d..97e7427 100644
--- a/pkg/analysis_server_client/lib/src/protocol/protocol_common.dart
+++ b/pkg/analysis_server_client/lib/src/protocol/protocol_common.dart
@@ -4373,8 +4373,14 @@
}
/// Adds [edit] to the [FileEdit] for the given [file].
- void addEdit(String file, int fileStamp, SourceEdit edit) =>
- addEditToSourceChange(this, file, fileStamp, edit);
+ ///
+ /// If [insertBeforeExisting] is `true`, inserts made at the same offset as
+ /// other edits will be inserted such that they appear before them in the
+ /// resulting document.
+ void addEdit(String file, int fileStamp, SourceEdit edit,
+ {bool insertBeforeExisting = false}) =>
+ addEditToSourceChange(this, file, fileStamp, edit,
+ insertBeforeExisting: insertBeforeExisting);
/// Adds the given [FileEdit].
void addFileEdit(SourceFileEdit edit) {
@@ -4597,10 +4603,22 @@
}
/// Adds the given [Edit] to the list.
- void add(SourceEdit edit) => addEditForSource(this, edit);
+ ///
+ /// If [insertBeforeExisting] is `true`, inserts made at the same offset as
+ /// other edits will be inserted such that they appear before them in the
+ /// resulting document.
+ void add(SourceEdit edit, {bool insertBeforeExisting = false}) =>
+ addEditForSource(this, edit, insertBeforeExisting: insertBeforeExisting);
/// Adds the given [Edit]s.
- void addAll(Iterable<SourceEdit> edits) => addAllEditsForSource(this, edits);
+ ///
+ /// If [insertBeforeExisting] is `true`, inserts made at the same offset as
+ /// other edits will be inserted such that they appear before them in the
+ /// resulting document.
+ void addAll(Iterable<SourceEdit> edits,
+ {bool insertBeforeExisting = false}) =>
+ addAllEditsForSource(this, edits,
+ insertBeforeExisting: insertBeforeExisting);
@override
String toString() => json.encode(toJson());
diff --git a/pkg/analysis_server_client/lib/src/protocol/protocol_internal.dart b/pkg/analysis_server_client/lib/src/protocol/protocol_internal.dart
index 399fb51..431cca2 100644
--- a/pkg/analysis_server_client/lib/src/protocol/protocol_internal.dart
+++ b/pkg/analysis_server_client/lib/src/protocol/protocol_internal.dart
@@ -13,30 +13,54 @@
HashMap<String, RefactoringKind>();
/// Adds the given [sourceEdits] to the list in [sourceFileEdit].
+///
+/// If [insertBeforeExisting] is `true`, inserts made at the same offset as
+/// other edits will be inserted such that they appear before them in the
+/// resulting document.
void addAllEditsForSource(
- SourceFileEdit sourceFileEdit, Iterable<SourceEdit> edits) {
- edits.forEach(sourceFileEdit.add);
+ SourceFileEdit sourceFileEdit, Iterable<SourceEdit> edits,
+ {bool insertBeforeExisting = false}) {
+ edits.forEach((edit) =>
+ sourceFileEdit.add(edit, insertBeforeExisting: insertBeforeExisting));
}
/// Adds the given [sourceEdit] to the list in [sourceFileEdit].
-void addEditForSource(SourceFileEdit sourceFileEdit, SourceEdit sourceEdit) {
+///
+/// If [insertBeforeExisting] is `true`, inserts made at the same offset as
+/// other edits will be inserted such that they appear before them in the
+/// resulting document.
+void addEditForSource(SourceFileEdit sourceFileEdit, SourceEdit sourceEdit,
+ {bool insertBeforeExisting = false}) {
var edits = sourceFileEdit.edits;
+ var length = edits.length;
var index = 0;
- while (index < edits.length && edits[index].offset > sourceEdit.offset) {
+ while (index < length && edits[index].offset > sourceEdit.offset) {
index++;
}
+ // If it's an insert and it should be inserted before existing edits, also
+ // skip over any with the same offset.
+ if (insertBeforeExisting && sourceEdit.length == 0) {
+ while (index < length && edits[index].offset >= sourceEdit.offset) {
+ index++;
+ }
+ }
edits.insert(index, sourceEdit);
}
/// Adds [edit] to the [FileEdit] for the given [file].
+///
+/// If [insertBeforeExisting] is `true`, inserts made at the same offset as
+/// other edits will be inserted such that they appear before them in the
+/// resulting document.
void addEditToSourceChange(
- SourceChange change, String file, int fileStamp, SourceEdit edit) {
+ SourceChange change, String file, int fileStamp, SourceEdit edit,
+ {bool insertBeforeExisting = false}) {
var fileEdit = change.getFileEdit(file);
if (fileEdit == null) {
fileEdit = SourceFileEdit(file, fileStamp);
change.addFileEdit(fileEdit);
}
- fileEdit.add(edit);
+ fileEdit.add(edit, insertBeforeExisting: insertBeforeExisting);
}
/// Get the result of applying the edit to the given [code]. Access via
diff --git a/pkg/analyzer_plugin/lib/protocol/protocol_common.dart b/pkg/analyzer_plugin/lib/protocol/protocol_common.dart
index 5ca19e2..32fe3b2 100644
--- a/pkg/analyzer_plugin/lib/protocol/protocol_common.dart
+++ b/pkg/analyzer_plugin/lib/protocol/protocol_common.dart
@@ -4373,8 +4373,14 @@
}
/// Adds [edit] to the [FileEdit] for the given [file].
- void addEdit(String file, int fileStamp, SourceEdit edit) =>
- addEditToSourceChange(this, file, fileStamp, edit);
+ ///
+ /// If [insertBeforeExisting] is `true`, inserts made at the same offset as
+ /// other edits will be inserted such that they appear before them in the
+ /// resulting document.
+ void addEdit(String file, int fileStamp, SourceEdit edit,
+ {bool insertBeforeExisting = false}) =>
+ addEditToSourceChange(this, file, fileStamp, edit,
+ insertBeforeExisting: insertBeforeExisting);
/// Adds the given [FileEdit].
void addFileEdit(SourceFileEdit edit) {
@@ -4597,10 +4603,22 @@
}
/// Adds the given [Edit] to the list.
- void add(SourceEdit edit) => addEditForSource(this, edit);
+ ///
+ /// If [insertBeforeExisting] is `true`, inserts made at the same offset as
+ /// other edits will be inserted such that they appear before them in the
+ /// resulting document.
+ void add(SourceEdit edit, {bool insertBeforeExisting = false}) =>
+ addEditForSource(this, edit, insertBeforeExisting: insertBeforeExisting);
/// Adds the given [Edit]s.
- void addAll(Iterable<SourceEdit> edits) => addAllEditsForSource(this, edits);
+ ///
+ /// If [insertBeforeExisting] is `true`, inserts made at the same offset as
+ /// other edits will be inserted such that they appear before them in the
+ /// resulting document.
+ void addAll(Iterable<SourceEdit> edits,
+ {bool insertBeforeExisting = false}) =>
+ addAllEditsForSource(this, edits,
+ insertBeforeExisting: insertBeforeExisting);
@override
String toString() => json.encode(toJson());
diff --git a/pkg/analyzer_plugin/lib/src/protocol/protocol_internal.dart b/pkg/analyzer_plugin/lib/src/protocol/protocol_internal.dart
index 777ccda..262911a 100644
--- a/pkg/analyzer_plugin/lib/src/protocol/protocol_internal.dart
+++ b/pkg/analyzer_plugin/lib/src/protocol/protocol_internal.dart
@@ -14,26 +14,43 @@
HashMap<String, RefactoringKind>();
/// Adds the given [sourceEdits] to the list in [sourceFileEdit].
+///
+/// If [insertBeforeExisting] is `true`, inserts made at the same offset as
+/// other edits will be inserted such that they appear before them in the
+/// resulting document.
void addAllEditsForSource(
- SourceFileEdit sourceFileEdit, Iterable<SourceEdit> edits) {
- edits.forEach(sourceFileEdit.add);
+ SourceFileEdit sourceFileEdit, Iterable<SourceEdit> edits,
+ {bool insertBeforeExisting = false}) {
+ edits.forEach((edit) =>
+ sourceFileEdit.add(edit, insertBeforeExisting: insertBeforeExisting));
}
/// Adds the given [sourceEdit] to the list in [sourceFileEdit] while preserving
-/// two invariants:
+/// the invariants:
/// - the list is sorted such that edits with a larger offset appear earlier in
/// the list, and
-/// - no two edits in the list overlap each other.
+/// - no two edits in the list overlap each other, and
+/// - inserts can only be made at the same offset as an earlier edit when
+/// [insertBeforeExisting] is `true` and will result in the inserted text
+/// appearing before the other edits in the resulting document.
///
/// If the invariants can't be preserved, then a [ConflictingEditException] is
/// thrown.
-void addEditForSource(SourceFileEdit sourceFileEdit, SourceEdit sourceEdit) {
+void addEditForSource(SourceFileEdit sourceFileEdit, SourceEdit sourceEdit,
+ {bool insertBeforeExisting = false}) {
var edits = sourceFileEdit.edits;
var length = edits.length;
var index = 0;
while (index < length && edits[index].offset > sourceEdit.offset) {
index++;
}
+ // If it's an insert and it should be inserted before existing edits, also
+ // skip over any with the same offset.
+ if (insertBeforeExisting && sourceEdit.length == 0) {
+ while (index < length && edits[index].offset >= sourceEdit.offset) {
+ index++;
+ }
+ }
if (index > 0) {
var previousEdit = edits[index - 1];
// The [previousEdit] has an offset that is strictly greater than the offset
@@ -63,14 +80,19 @@
}
/// Adds [edit] to the [FileEdit] for the given [file].
+///
+/// If [insertBeforeExisting] is `true`, inserts made at the same offset as
+/// other edits will be inserted such that they appear before them in the
+/// resulting document.
void addEditToSourceChange(
- SourceChange change, String file, int fileStamp, SourceEdit edit) {
+ SourceChange change, String file, int fileStamp, SourceEdit edit,
+ {bool insertBeforeExisting = false}) {
var fileEdit = change.getFileEdit(file);
if (fileEdit == null) {
fileEdit = SourceFileEdit(file, fileStamp);
change.addFileEdit(fileEdit);
}
- fileEdit.add(edit);
+ fileEdit.add(edit, insertBeforeExisting: insertBeforeExisting);
}
/// Get the result of applying the edit to the given [code]. Access via
diff --git a/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_core.dart b/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_core.dart
index a1bd021..be0b7fc 100644
--- a/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_core.dart
+++ b/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_core.dart
@@ -458,12 +458,13 @@
}
@override
- void addInsertion(int offset, void Function(EditBuilder builder) buildEdit) {
+ void addInsertion(int offset, void Function(EditBuilder builder) buildEdit,
+ {bool insertBeforeExisting = false}) {
var builder = createEditBuilder(offset, 0);
try {
buildEdit(builder);
} finally {
- _addEditBuilder(builder);
+ _addEditBuilder(builder, insertBeforeExisting: insertBeforeExisting);
}
}
@@ -542,8 +543,8 @@
/// Add the edit from the given [edit] to the edits associated with the
/// current file.
- void _addEdit(SourceEdit edit) {
- fileEdit.add(edit);
+ void _addEdit(SourceEdit edit, {bool insertBeforeExisting = false}) {
+ fileEdit.add(edit, insertBeforeExisting: insertBeforeExisting);
var delta = _editDelta(edit);
changeBuilder._updatePositions(edit.offset, delta);
changeBuilder._lockedPositions.clear();
@@ -551,9 +552,14 @@
/// Add the edit from the given [builder] to the edits associated with the
/// current file.
- void _addEditBuilder(EditBuilderImpl builder) {
+ ///
+ /// If [insertBeforeExisting] is `true`, inserts made at the same offset as
+ /// other edits will be inserted such that they appear before them in the
+ /// resulting document.
+ void _addEditBuilder(EditBuilderImpl builder,
+ {bool insertBeforeExisting = false}) {
var edit = builder.sourceEdit;
- _addEdit(edit);
+ _addEdit(edit, insertBeforeExisting: insertBeforeExisting);
_captureSelection(builder, edit);
}
diff --git a/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart b/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart
index cee17a3..3397ab6 100644
--- a/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart
+++ b/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart
@@ -1356,9 +1356,11 @@
@override
void addInsertion(
- int offset, void Function(DartEditBuilder builder) buildEdit) =>
+ int offset, void Function(DartEditBuilder builder) buildEdit,
+ {bool insertBeforeExisting = false}) =>
super.addInsertion(
- offset, (builder) => buildEdit(builder as DartEditBuilder));
+ offset, (builder) => buildEdit(builder as DartEditBuilder),
+ insertBeforeExisting: insertBeforeExisting);
@override
void addReplacement(SourceRange range,
@@ -1679,19 +1681,36 @@
if (unit.declarations.isNotEmpty) {
offset = unit.declarations.first.offset;
insertEmptyLineAfter = true;
+ } else if (fileEdit.edits.isNotEmpty) {
+ // If this file has edits (besides the imports) the imports should go
+ // at the same offset as those edits and _not_ at `unit.end`. This is
+ // because if the document is non-zero length, `unit.end` could be after
+ // where the new edits will be inserted, but imports should go before
+ // generated non-import code.
+
+ // Edits are always sorted such that the first one has the lowest offset.
+ offset = fileEdit.edits.first.offset;
+
+ // Also ensure there's a blank line between the imports and the other
+ // code.
+ insertEmptyLineAfter = fileEdit.edits.isNotEmpty;
} else {
offset = unit.end;
}
- addInsertion(offset, (EditBuilder builder) {
- for (var i = 0; i < importList.length; i++) {
- var import = importList[i];
- writeImport(builder, import);
- builder.writeln();
- if (i == importList.length - 1 && insertEmptyLineAfter) {
+ addInsertion(
+ offset,
+ (EditBuilder builder) {
+ for (var i = 0; i < importList.length; i++) {
+ var import = importList[i];
+ writeImport(builder, import);
builder.writeln();
+ if (i == importList.length - 1 && insertEmptyLineAfter) {
+ builder.writeln();
+ }
}
- }
- });
+ },
+ insertBeforeExisting: true,
+ );
}
/// Return the import element used to import the given [element] into the
diff --git a/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_yaml.dart b/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_yaml.dart
index 6e37763..a941bae 100644
--- a/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_yaml.dart
+++ b/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_yaml.dart
@@ -48,9 +48,11 @@
@override
void addInsertion(
- int offset, void Function(YamlEditBuilder builder) buildEdit) =>
+ int offset, void Function(YamlEditBuilder builder) buildEdit,
+ {bool insertBeforeExisting = false}) =>
super.addInsertion(
- offset, (builder) => buildEdit(builder as YamlEditBuilder));
+ offset, (builder) => buildEdit(builder as YamlEditBuilder),
+ insertBeforeExisting: insertBeforeExisting);
@override
void addReplacement(SourceRange range,
diff --git a/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_dart_test.dart b/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_dart_test.dart
index c7211d4..bd2700d 100644
--- a/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_dart_test.dart
+++ b/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_dart_test.dart
@@ -678,9 +678,10 @@
expect(edit.replacement, equalsIgnoringWhitespace('Foo'));
}
- Future<void> test_writeImportedName_needsImport() async {
+ Future<void> test_writeImportedName_needsImport_insertion() async {
var path = convertPath('/home/test/lib/test.dart');
- addSource(path, '');
+ var content = '';
+ addSource(path, content);
var builder = newBuilder();
await builder.addDartFileEdit(path, (builder) {
@@ -693,9 +694,40 @@
});
var edits = getEdits(builder);
expect(edits, hasLength(2));
- expect(edits[0].replacement,
+ expect(edits[0].replacement, equalsIgnoringWhitespace('Foo'));
+ expect(edits[1].replacement,
equalsIgnoringWhitespace("import 'package:test/foo.dart';\n"));
- expect(edits[1].replacement, equalsIgnoringWhitespace('Foo'));
+ var result = SourceEdit.applySequence(content, edits);
+ expect(result, '''
+import 'package:test/foo.dart';
+
+Foo''');
+ }
+
+ Future<void> test_writeImportedName_needsImport_replacement() async {
+ var path = convertPath('/home/test/lib/test.dart');
+ var content = 'test';
+ addSource(path, content);
+
+ var builder = newBuilder();
+ await builder.addDartFileEdit(path, (builder) {
+ builder.addReplacement(SourceRange(0, 4), (builder) {
+ builder.writeImportedName([
+ Uri.parse('package:test/foo.dart'),
+ Uri.parse('package:test/bar.dart')
+ ], 'Foo');
+ });
+ });
+ var edits = getEdits(builder);
+ expect(edits, hasLength(2));
+ expect(edits[0].replacement, equalsIgnoringWhitespace('Foo'));
+ expect(edits[1].replacement,
+ equalsIgnoringWhitespace("import 'package:test/foo.dart';\n"));
+ var result = SourceEdit.applySequence(content, edits);
+ expect(result, '''
+import 'package:test/foo.dart';
+
+Foo''');
}
Future<void> test_writeLocalVariableDeclaration_noType_initializer() async {
@@ -1257,14 +1289,19 @@
var builder = newBuilder();
await builder.addDartFileEdit(path, (builder) {
- builder.addInsertion(content.length - 1, (builder) {
+ builder.addInsertion(0, (builder) {
builder.writeReference(aElement);
});
});
var edits = getEdits(builder);
expect(edits, hasLength(2));
- expect(edits[0].replacement, equalsIgnoringWhitespace("import 'a.dart';"));
- expect(edits[1].replacement, equalsIgnoringWhitespace('a'));
+ expect(edits[0].replacement, equalsIgnoringWhitespace('a'));
+ expect(edits[1].replacement, equalsIgnoringWhitespace("import 'a.dart';"));
+ var result = SourceEdit.applySequence(content, edits);
+ expect(result, '''
+import 'a.dart';
+
+a''');
}
Future<void> test_writeSetterDeclaration_bodyWriter() async {
@@ -1528,7 +1565,7 @@
var builder = newBuilder();
await builder.addDartFileEdit(path, (builder) {
- builder.addInsertion(content.length - 1, (builder) {
+ builder.addInsertion(0, (builder) {
builder.writeType(a1.instantiate(
typeArguments: [],
nullabilitySuffix: NullabilitySuffix.star,
@@ -1552,12 +1589,19 @@
expect(edits, hasLength(2));
expect(
edits[0].replacement,
- equalsIgnoringWhitespace("import 'package:test/a.dart' as _prefix0; "
- "import 'package:test/b.dart' as _prefix1;"));
- expect(
- edits[1].replacement,
equalsIgnoringWhitespace(
'_prefix0.A1 a1; _prefix0.A2 a2; _prefix1.B b;'));
+ expect(
+ edits[1].replacement,
+ equalsIgnoringWhitespace("import 'package:test/a.dart' as _prefix0; "
+ "import 'package:test/b.dart' as _prefix1;"));
+
+ var resultCode = SourceEdit.applySequence(content, edits);
+ expect(resultCode, r'''
+import 'package:test/a.dart' as _prefix0;
+import 'package:test/b.dart' as _prefix1;
+
+_prefix0.A1 a1; _prefix0.A2 a2; _prefix1.B b;''');
}
Future<void> test_writeType_required_dynamic() async {
diff --git a/pkg/analyzer_plugin/tool/spec/codegen_dart_protocol.dart b/pkg/analyzer_plugin/tool/spec/codegen_dart_protocol.dart
index c5f4cb9..e99476e 100644
--- a/pkg/analyzer_plugin/tool/spec/codegen_dart_protocol.dart
+++ b/pkg/analyzer_plugin/tool/spec/codegen_dart_protocol.dart
@@ -713,10 +713,20 @@
writeln('}');
return true;
case 'SourceChange':
- docComment(
- [dom.Text('Adds [edit] to the [FileEdit] for the given [file].')]);
- writeln('void addEdit(String file, int fileStamp, SourceEdit edit) =>');
- writeln(' addEditToSourceChange(this, file, fileStamp, edit);');
+ docComment([
+ dom.Element.tag('p')
+ ..append(dom.Text(
+ 'Adds [edit] to the [FileEdit] for the given [file].')),
+ dom.Element.tag('p')
+ ..append(dom.Text(
+ 'If [insertBeforeExisting] is `true`, inserts made at the '
+ 'same offset as other edits will be inserted such that they '
+ 'appear before them in the resulting document.')),
+ ]);
+ writeln('void addEdit(String file, int fileStamp, SourceEdit edit, '
+ '{bool insertBeforeExisting = false}) =>');
+ writeln(' addEditToSourceChange(this, file, fileStamp, edit, '
+ 'insertBeforeExisting: insertBeforeExisting);');
writeln();
docComment([dom.Text('Adds the given [FileEdit].')]);
writeln('void addFileEdit(SourceFileEdit edit) {');
@@ -745,12 +755,32 @@
writeln('String apply(String code) => applyEdit(code, this);');
return true;
case 'SourceFileEdit':
- docComment([dom.Text('Adds the given [Edit] to the list.')]);
- writeln('void add(SourceEdit edit) => addEditForSource(this, edit);');
+ docComment([
+ dom.Element.tag('p')
+ ..append(dom.Text('Adds the given [Edit] to the list.')),
+ dom.Element.tag('p')
+ ..append(dom.Text(
+ 'If [insertBeforeExisting] is `true`, inserts made at the '
+ 'same offset as other edits will be inserted such that they '
+ 'appear before them in the resulting document.')),
+ ]);
+ writeln('void add(SourceEdit edit, '
+ '{bool insertBeforeExisting = false}) =>');
+ writeln(' addEditForSource(this, edit, '
+ 'insertBeforeExisting: insertBeforeExisting);');
writeln();
- docComment([dom.Text('Adds the given [Edit]s.')]);
- writeln('void addAll(Iterable<SourceEdit> edits) =>');
- writeln(' addAllEditsForSource(this, edits);');
+ docComment([
+ dom.Element.tag('p')..append(dom.Text('Adds the given [Edit]s.')),
+ dom.Element.tag('p')
+ ..append(dom.Text(
+ 'If [insertBeforeExisting] is `true`, inserts made at the '
+ 'same offset as other edits will be inserted such that they '
+ 'appear before them in the resulting document.')),
+ ]);
+ writeln('void addAll(Iterable<SourceEdit> edits, '
+ '{bool insertBeforeExisting = false}) =>');
+ writeln(' addAllEditsForSource(this, edits, '
+ 'insertBeforeExisting: insertBeforeExisting);');
return true;
default:
return false;
diff --git a/tools/VERSION b/tools/VERSION
index e69a453..25a16b1 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
MAJOR 2
MINOR 17
PATCH 0
-PRERELEASE 163
+PRERELEASE 164
PRERELEASE_PATCH 0
\ No newline at end of file