Version 2.18.0-224.0.dev

Merge commit '8596955593815cc72f6f65fe1669a63805f0671d' into 'dev'
diff --git a/pkg/analysis_server/test/lsp/code_actions_abstract.dart b/pkg/analysis_server/test/lsp/code_actions_abstract.dart
index 2f059a1..c4d4e68 100644
--- a/pkg/analysis_server/test/lsp/code_actions_abstract.dart
+++ b/pkg/analysis_server/test/lsp/code_actions_abstract.dart
@@ -13,10 +13,13 @@
     Uri uri,
     String command,
     String title, {
+    Range? range,
+    Position? position,
     bool asCodeActionLiteral = false,
     bool asCommand = false,
   }) async {
-    final codeActions = await getCodeActions(uri.toString());
+    final codeActions =
+        await getCodeActions(uri.toString(), range: range, position: position);
     final codeAction = findCommand(codeActions, command)!;
 
     codeAction.map(
diff --git a/pkg/analysis_server/test/lsp/code_actions_fixes_test.dart b/pkg/analysis_server/test/lsp/code_actions_fixes_test.dart
index d058b29..3145f67 100644
--- a/pkg/analysis_server/test/lsp/code_actions_fixes_test.dart
+++ b/pkg/analysis_server/test/lsp/code_actions_fixes_test.dart
@@ -463,7 +463,10 @@
           emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
     );
 
-    final codeActions = await getCodeActions(otherFileUri.toString());
+    final codeActions = await getCodeActions(
+      otherFileUri.toString(),
+      position: startOfDocPos,
+    );
     expect(codeActions, isEmpty);
   }
 
diff --git a/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart b/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart
index 9a6951c..d580d4e 100644
--- a/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart
+++ b/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart
@@ -354,10 +354,11 @@
 ^
 void f() {}
     ''';
-    newFile(mainFilePath, content);
+    newFile(mainFilePath, withoutMarkers(content));
     await initialize();
 
-    final codeActions = await getCodeActions(mainFileUri.toString());
+    final codeActions = await getCodeActions(mainFileUri.toString(),
+        position: positionFromMarker(content));
     final codeAction =
         findCommand(codeActions, Commands.performRefactor, extractMethodTitle);
     expect(codeAction, isNull);
@@ -743,10 +744,11 @@
 ^
 void f() {}
     ''';
-    newFile(mainFilePath, content);
+    newFile(mainFilePath, withoutMarkers(content));
     await initialize();
 
-    final codeActions = await getCodeActions(mainFileUri.toString());
+    final codeActions = await getCodeActions(mainFileUri.toString(),
+        position: positionFromMarker(content));
     final codeAction =
         findCommand(codeActions, Commands.performRefactor, extractWidgetTitle);
     expect(codeAction, isNull);
diff --git a/pkg/analysis_server/test/lsp/code_actions_source_test.dart b/pkg/analysis_server/test/lsp/code_actions_source_test.dart
index dc3fd88..b803a3c 100644
--- a/pkg/analysis_server/test/lsp/code_actions_source_test.dart
+++ b/pkg/analysis_server/test/lsp/code_actions_source_test.dart
@@ -19,8 +19,44 @@
   });
 }
 
+abstract class AbstractSourceCodeActionsTest extends AbstractCodeActionsTest {
+  /// Wrapper around [checkCodeActionAvailable] for Source actions where
+  /// position/range is irrelevant (so uses [startOfDocPos]).
+  Future<void> checkSourceCodeActionAvailable(
+    Uri uri,
+    String command,
+    String title, {
+    bool asCodeActionLiteral = false,
+    bool asCommand = false,
+  }) async {
+    return checkCodeActionAvailable(
+      uri,
+      command,
+      title,
+      position: startOfDocPos,
+      asCodeActionLiteral: asCodeActionLiteral,
+      asCommand: asCommand,
+    );
+  }
+
+  /// Wrapper around [getCodeActions] for Source actions where position/range is
+  /// irrelevant (so uses [startOfDocPos]).
+  Future<List<Either2<Command, CodeAction>>> getSourceCodeActions(
+    String fileUri, {
+    List<CodeActionKind>? kinds,
+    CodeActionTriggerKind? triggerKind,
+  }) {
+    return getCodeActions(
+      fileUri,
+      position: startOfDocPos,
+      kinds: kinds,
+      triggerKind: triggerKind,
+    );
+  }
+}
+
 @reflectiveTest
-class FixAllSourceCodeActionsTest extends AbstractCodeActionsTest {
+class FixAllSourceCodeActionsTest extends AbstractSourceCodeActionsTest {
   Future<void> test_appliesCorrectEdits() async {
     const analysisOptionsContent = '''
 linter:
@@ -45,7 +81,7 @@
         workspaceCapabilities:
             withApplyEditSupport(emptyWorkspaceClientCapabilities));
 
-    final codeActions = await getCodeActions(mainFileUri.toString());
+    final codeActions = await getSourceCodeActions(mainFileUri.toString());
     final codeAction = findCommand(codeActions, Commands.fixAll)!;
 
     await verifyCodeActionEdits(codeAction, content, expectedContent);
@@ -53,7 +89,8 @@
 }
 
 @reflectiveTest
-class OrganizeImportsSourceCodeActionsTest extends AbstractCodeActionsTest {
+class OrganizeImportsSourceCodeActionsTest
+    extends AbstractSourceCodeActionsTest {
   Future<void> test_appliesCorrectEdits_withDocumentChangesSupport() async {
     const content = '''
 import 'dart:math';
@@ -75,7 +112,7 @@
         workspaceCapabilities: withApplyEditSupport(
             withDocumentChangesSupport(emptyWorkspaceClientCapabilities)));
 
-    final codeActions = await getCodeActions(mainFileUri.toString());
+    final codeActions = await getSourceCodeActions(mainFileUri.toString());
     final codeAction = findCommand(codeActions, Commands.organizeImports)!;
 
     await verifyCodeActionEdits(codeAction, content, expectedContent,
@@ -103,7 +140,7 @@
         workspaceCapabilities:
             withApplyEditSupport(emptyWorkspaceClientCapabilities));
 
-    final codeActions = await getCodeActions(mainFileUri.toString());
+    final codeActions = await getSourceCodeActions(mainFileUri.toString());
     final codeAction = findCommand(codeActions, Commands.organizeImports)!;
 
     await verifyCodeActionEdits(codeAction, content, expectedContent);
@@ -117,7 +154,7 @@
         workspaceCapabilities:
             withApplyEditSupport(emptyWorkspaceClientCapabilities));
 
-    await checkCodeActionAvailable(
+    await checkSourceCodeActionAvailable(
       mainFileUri,
       Commands.organizeImports,
       'Organize Imports',
@@ -131,7 +168,7 @@
         workspaceCapabilities:
             withApplyEditSupport(emptyWorkspaceClientCapabilities));
 
-    await checkCodeActionAvailable(
+    await checkSourceCodeActionAvailable(
       mainFileUri,
       Commands.organizeImports,
       'Organize Imports',
@@ -146,7 +183,7 @@
         workspaceCapabilities:
             withApplyEditSupport(emptyWorkspaceClientCapabilities));
 
-    final codeActions = await getCodeActions(
+    final codeActions = await getSourceCodeActions(
       mainFileUri.toString(),
       triggerKind: CodeActionTriggerKind.Automatic,
     );
@@ -169,7 +206,7 @@
         workspaceCapabilities:
             withApplyEditSupport(emptyWorkspaceClientCapabilities));
 
-    final codeActions = await getCodeActions(mainFileUri.toString());
+    final codeActions = await getSourceCodeActions(mainFileUri.toString());
     final codeAction = findCommand(codeActions, Commands.organizeImports)!;
 
     final command = codeAction.map(
@@ -189,7 +226,7 @@
         workspaceCapabilities:
             withApplyEditSupport(emptyWorkspaceClientCapabilities));
 
-    ofKind(CodeActionKind kind) => getCodeActions(
+    ofKind(CodeActionKind kind) => getSourceCodeActions(
           mainFileUri.toString(),
           kinds: [kind],
         );
@@ -215,7 +252,7 @@
         workspaceCapabilities:
             withApplyEditSupport(emptyWorkspaceClientCapabilities));
 
-    final codeActions = await getCodeActions(mainFileUri.toString());
+    final codeActions = await getSourceCodeActions(mainFileUri.toString());
     final codeAction = findCommand(codeActions, Commands.organizeImports)!;
 
     final command = codeAction.map(
@@ -238,7 +275,7 @@
         workspaceCapabilities:
             withApplyEditSupport(emptyWorkspaceClientCapabilities));
 
-    final codeActions = await getCodeActions(mainFileUri.toString());
+    final codeActions = await getSourceCodeActions(mainFileUri.toString());
     final codeAction = findCommand(codeActions, Commands.organizeImports);
     expect(codeAction, isNull);
   }
@@ -247,14 +284,14 @@
     newFile(mainFilePath, '');
     await initialize();
 
-    final codeActions = await getCodeActions(mainFileUri.toString());
+    final codeActions = await getSourceCodeActions(mainFileUri.toString());
     final codeAction = findCommand(codeActions, Commands.organizeImports);
     expect(codeAction, isNull);
   }
 }
 
 @reflectiveTest
-class SortMembersSourceCodeActionsTest extends AbstractCodeActionsTest {
+class SortMembersSourceCodeActionsTest extends AbstractSourceCodeActionsTest {
   Future<void> test_appliesCorrectEdits_withDocumentChangesSupport() async {
     const content = '''
     String b;
@@ -269,7 +306,7 @@
         workspaceCapabilities: withApplyEditSupport(
             withDocumentChangesSupport(emptyWorkspaceClientCapabilities)));
 
-    final codeActions = await getCodeActions(mainFileUri.toString());
+    final codeActions = await getSourceCodeActions(mainFileUri.toString());
     final codeAction = findCommand(codeActions, Commands.sortMembers)!;
 
     await verifyCodeActionEdits(codeAction, content, expectedContent,
@@ -290,7 +327,7 @@
         workspaceCapabilities:
             withApplyEditSupport(emptyWorkspaceClientCapabilities));
 
-    final codeActions = await getCodeActions(mainFileUri.toString());
+    final codeActions = await getSourceCodeActions(mainFileUri.toString());
     final codeAction = findCommand(codeActions, Commands.sortMembers)!;
 
     await verifyCodeActionEdits(codeAction, content, expectedContent);
@@ -304,7 +341,7 @@
         workspaceCapabilities:
             withApplyEditSupport(emptyWorkspaceClientCapabilities));
 
-    await checkCodeActionAvailable(
+    await checkSourceCodeActionAvailable(
       mainFileUri,
       Commands.sortMembers,
       'Sort Members',
@@ -318,7 +355,7 @@
         workspaceCapabilities:
             withApplyEditSupport(emptyWorkspaceClientCapabilities));
 
-    await checkCodeActionAvailable(
+    await checkSourceCodeActionAvailable(
       mainFileUri,
       Commands.sortMembers,
       'Sort Members',
@@ -336,7 +373,7 @@
         workspaceCapabilities:
             withApplyEditSupport(emptyWorkspaceClientCapabilities));
 
-    final codeActions = await getCodeActions(mainFileUri.toString());
+    final codeActions = await getSourceCodeActions(mainFileUri.toString());
     final codeAction = findCommand(codeActions, Commands.sortMembers)!;
 
     final command = codeAction.map(
@@ -369,7 +406,7 @@
         workspaceCapabilities:
             withApplyEditSupport(emptyWorkspaceClientCapabilities));
 
-    final codeActions = await getCodeActions(
+    final codeActions = await getSourceCodeActions(
       mainFileUri.toString(),
       triggerKind: CodeActionTriggerKind.Automatic,
     );
@@ -392,7 +429,7 @@
         workspaceCapabilities:
             withApplyEditSupport(emptyWorkspaceClientCapabilities));
 
-    final codeActions = await getCodeActions(mainFileUri.toString());
+    final codeActions = await getSourceCodeActions(mainFileUri.toString());
     final codeAction = findCommand(codeActions, Commands.sortMembers)!;
 
     final command = codeAction.map(
@@ -414,8 +451,7 @@
         workspaceCapabilities:
             withApplyEditSupport(emptyWorkspaceClientCapabilities));
 
-    final codeActions =
-        await getCodeActions(pubspecFileUri.toString(), range: startOfDocRange);
+    final codeActions = await getSourceCodeActions(pubspecFileUri.toString());
     expect(codeActions, isEmpty);
   }
 
@@ -427,7 +463,7 @@
         workspaceCapabilities:
             withApplyEditSupport(emptyWorkspaceClientCapabilities));
 
-    final codeActions = await getCodeActions(mainFileUri.toString());
+    final codeActions = await getSourceCodeActions(mainFileUri.toString());
     final codeAction = findCommand(codeActions, Commands.sortMembers);
     expect(codeAction, isNull);
   }
@@ -436,7 +472,7 @@
     newFile(mainFilePath, '');
     await initialize();
 
-    final codeActions = await getCodeActions(mainFileUri.toString());
+    final codeActions = await getSourceCodeActions(mainFileUri.toString());
     final codeAction = findCommand(codeActions, Commands.sortMembers);
     expect(codeAction, isNull);
   }
diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart
index 9a119b2..a28d399 100644
--- a/pkg/analysis_server/test/lsp/server_abstract.dart
+++ b/pkg/analysis_server/test/lsp/server_abstract.dart
@@ -1016,7 +1016,7 @@
   }) {
     range ??= position != null
         ? Range(start: position, end: position)
-        : startOfDocRange;
+        : throw 'Supply either a Range or Position for CodeActions requests';
     final request = makeRequest(
       Method.textDocument_codeAction,
       CodeActionParams(
diff --git a/pkg/dds/CHANGELOG.md b/pkg/dds/CHANGELOG.md
index 9a63d21..d1f139e 100644
--- a/pkg/dds/CHANGELOG.md
+++ b/pkg/dds/CHANGELOG.md
@@ -1,3 +1,6 @@
+# 2.2.4
+- Fix an issue where DAP adapters could try to remove the same breakpoint multiple times.
+
 # 2.2.3
 - Internal DAP changes.
 
diff --git a/pkg/dds/lib/src/dap/isolate_manager.dart b/pkg/dds/lib/src/dap/isolate_manager.dart
index ff6fc4a..443d148 100644
--- a/pkg/dds/lib/src/dap/isolate_manager.dart
+++ b/pkg/dds/lib/src/dap/isolate_manager.dart
@@ -624,7 +624,12 @@
           _vmBreakpointsByIsolateIdAndUri.putIfAbsent(isolateId, () => {});
       final existingBreakpointsForIsolateAndUri =
           existingBreakpointsForIsolate.putIfAbsent(uri, () => []);
-      await Future.forEach<vm.Breakpoint>(existingBreakpointsForIsolateAndUri,
+      // Before doing async work, take a copy of the breakpoints to remove
+      // and remove them from the list, so any subsequent calls here don't
+      // try to remove the same ones.
+      final breakpointsToRemove = existingBreakpointsForIsolateAndUri.toList();
+      existingBreakpointsForIsolateAndUri.clear();
+      await Future.forEach<vm.Breakpoint>(breakpointsToRemove,
           (bp) => service.removeBreakpoint(isolateId, bp.id!));
 
       // Set new breakpoints.
diff --git a/pkg/dds/pubspec.yaml b/pkg/dds/pubspec.yaml
index 972dcf1..bf4a249 100644
--- a/pkg/dds/pubspec.yaml
+++ b/pkg/dds/pubspec.yaml
@@ -1,5 +1,5 @@
 name: dds
-version: 2.2.3
+version: 2.2.4
 description: >-
   A library used to spawn the Dart Developer Service, used to communicate with
   a Dart VM Service instance.
diff --git a/pkg/dds/test/dap/integration/debug_breakpoints_test.dart b/pkg/dds/test/dap/integration/debug_breakpoints_test.dart
index 3038e06..0c5ca76 100644
--- a/pkg/dds/test/dap/integration/debug_breakpoints_test.dart
+++ b/pkg/dds/test/dap/integration/debug_breakpoints_test.dart
@@ -27,6 +27,50 @@
       await client.hitBreakpoint(testFile, breakpointLine);
     });
 
+    test('does not stop at a removed breakpoint', () async {
+      final testFile = dap.createTestFile('''
+void main(List<String> args) async {
+  print('Hello!'); $breakpointMarker
+  print('Hello!'); $breakpointMarker
+}
+    ''');
+
+      final client = dap.client;
+      final breakpoint1Line = lineWith(testFile, breakpointMarker);
+      final breakpoint2Line = breakpoint1Line + 1;
+
+      // Hit the first breakpoint.
+      final stop = await client.hitBreakpoint(testFile, breakpoint1Line,
+          additionalBreakpoints: [breakpoint2Line]);
+
+      // Remove all breakpoints.
+      await client.setBreakpoints(testFile, []);
+
+      // Resume and expect termination (should not hit the second breakpoint).
+      await Future.wait([
+        client.event('terminated'),
+        client.continue_(stop.threadId!),
+      ], eagerError: true);
+    });
+
+    test('does not fail updating breakpoints after a removal', () async {
+      // https://github.com/flutter/flutter/issues/106369 was caused by us not
+      // tracking removals correctly, meaning we could try to remove a removed
+      // breakpoint a second time.
+      final client = dap.client;
+      final testFile = dap.createTestFile(simpleBreakpointProgram);
+      final breakpointLine = lineWith(testFile, breakpointMarker);
+
+      await client.hitBreakpoint(testFile, breakpointLine);
+
+      // Remove the breakpoint.
+      await client.setBreakpoints(testFile, []);
+
+      // Send another breakpoint update to ensure it doesn't try to re-remove
+      // the previously removed breakpoint.
+      await client.setBreakpoints(testFile, []);
+    });
+
     test('stops at a line breakpoint in the SDK set via local sources',
         () async {
       final client = dap.client;
diff --git a/pkg/dds/test/dap/integration/test_client.dart b/pkg/dds/test/dap/integration/test_client.dart
index 4fd429b..1dc5b9e 100644
--- a/pkg/dds/test/dap/integration/test_client.dart
+++ b/pkg/dds/test/dap/integration/test_client.dart
@@ -494,18 +494,24 @@
   Future<StoppedEventBody> hitBreakpoint(
     File file,
     int line, {
+    List<int>? additionalBreakpoints,
     File? entryFile,
     String? condition,
     String? cwd,
     List<String>? args,
     Future<Response> Function()? launch,
   }) async {
+    assert(condition == null || additionalBreakpoints == null,
+        'Only one of condition/additionalBreakpoints can be sent');
     entryFile ??= file;
     final stop = expectStop('breakpoint', file: file, line: line);
 
     await Future.wait([
       initialize(),
-      setBreakpoint(file, line, condition: condition),
+      if (additionalBreakpoints != null)
+        setBreakpoints(file, [line, ...additionalBreakpoints])
+      else
+        setBreakpoint(file, line, condition: condition),
       launch?.call() ?? this.launch(entryFile.path, cwd: cwd, args: args),
     ], eagerError: true);
 
@@ -522,6 +528,16 @@
     );
   }
 
+  /// Sets breakpoints at [lines] in [file].
+  Future<void> setBreakpoints(File file, List<int> lines) async {
+    await sendRequest(
+      SetBreakpointsArguments(
+        source: Source(path: file.path),
+        breakpoints: lines.map((line) => SourceBreakpoint(line: line)).toList(),
+      ),
+    );
+  }
+
   /// Sets the exception pause mode to [pauseMode] and expects to pause after
   /// running the script.
   ///
diff --git a/tools/VERSION b/tools/VERSION
index 157fc8a..cc2dd56 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 18
 PATCH 0
-PRERELEASE 223
+PRERELEASE 224
 PRERELEASE_PATCH 0
\ No newline at end of file