[ios,macos] Add Swift Sourcekit LSP support (#189761)

This adds a post-processing pass to `complie_commands.json` that
synthesizes the Swift entries that can be understood by SourceKit LSP
and wires up the editor config needed to pick them up.

GN's `--export-compile-commands` only understands the built-in
`cc`/`cxx`/`objc`/`objcxx` tool types, and doesn't yet have built-in
support for injecting `swiftc` lines, so our custom `swift` tool (which
invokes `swiftc.py`) never gets an entry there, meaning Swift code in
the iOS and macOS embedders isn't indexed by SourceKit LSP and other
tooling that reads that file.

Since GN's own compdb export omits the swift targets, `_postGn()` now
shells out (once) to `ninja -t compdb` for the full compilation database
(and falls back to the existing `compile_commands.json` on disk if that
fails), then runs it through `expandSwiftcCommands`, which turns each
`swiftc.py`-wrapped entry into one or more native `swiftc` invocations,
one per compiled Swift file, with relative paths resolved to absolute
against the entry's `directory`.

We drop `-isystem` and `-Dkey=value` flags since that's what `swiftc.py`
does; swiftc only accepts boolean -D defines not key=value flags.

This scans the raw ninja output directly rather than decoding all of
`compile_commands.json`, which can run past 20MB and is too slow to
parse wholesale in Dart; only the small handful of matched `swiftc.py`
entries get `jsonDecode`d -- fewer than 10 lines.

This also wires up `.sourcekit-lsp/config.json` and the
`swift.sourcekit-lsp.supported-languages` setting in
`engine.code-workspace`/`engine-workspace.yaml` so that VS Code's Swift
extension correctly picks up the generated compdb.

Fixes https://github.com/flutter/flutter/issues/185741

## Pre-launch Checklist

- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [X] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [X] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [X] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [X] I signed the [CLA].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [X] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [X] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [X] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

If this change needs to override an active code freeze, provide a
comment explaining why. The code freeze workflow can be overridden by
code reviewers. See pinned issues for any active code freezes with
guidance.

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
diff --git a/engine/src/flutter/.sourcekit-lsp/config.json b/engine/src/flutter/.sourcekit-lsp/config.json
new file mode 100644
index 0000000..e7d11af
--- /dev/null
+++ b/engine/src/flutter/.sourcekit-lsp/config.json
@@ -0,0 +1,7 @@
+{
+  "compilationDatabase": {
+    "searchPaths": [
+      "../"
+    ]
+  }
+}
diff --git a/engine/src/flutter/engine.code-workspace b/engine/src/flutter/engine.code-workspace
index b3c6a99..f267201 100644
--- a/engine/src/flutter/engine.code-workspace
+++ b/engine/src/flutter/engine.code-workspace
@@ -137,6 +137,9 @@
     ],
     "dotnet.defaultSolution": "disable",
     "dart.showTodos": false,
+    "swift.sourcekit-lsp.supported-languages": [
+      "swift"
+    ],
     "testMate.cpp.test.advancedExecutables": [
       {
         "name": "impeller_unittests_arm64",
diff --git a/engine/src/flutter/tools/clang_tidy/test/header_filter_regex_test.dart b/engine/src/flutter/tools/clang_tidy/test/header_filter_regex_test.dart
index 0d97ec4..3abde0f 100644
--- a/engine/src/flutter/tools/clang_tidy/test/header_filter_regex_test.dart
+++ b/engine/src/flutter/tools/clang_tidy/test/header_filter_regex_test.dart
@@ -40,6 +40,7 @@
       '.git',
       '.gemini',
       '.github',
+      '.sourcekit-lsp',
       '.vscode',
       'build_overrides',
       'buildtools',
diff --git a/engine/src/flutter/tools/engine_tool/test/commands/build_command_test.dart b/engine/src/flutter/tools/engine_tool/test/commands/build_command_test.dart
index 99d15ef..bba1b89 100644
--- a/engine/src/flutter/tools/engine_tool/test/commands/build_command_test.dart
+++ b/engine/src/flutter/tools/engine_tool/test/commands/build_command_test.dart
@@ -54,7 +54,10 @@
     printOnFailure(testEnv.testLogs.map((r) => r.message).join('\n'));
     expect(result, equals(0));
     expect(testEnv.processHistory.length, greaterThanOrEqualTo(2));
-    expect(testEnv.processHistory[1].command[0], contains('ninja'));
+    final ExecutedProcess ninja = testEnv.processHistory.firstWhere(
+      (p) => p.command.contains('-C'),
+    );
+    expect(ninja.command[0], contains('ninja'));
   });
 
   test('build command invokes generator', () async {
@@ -127,7 +130,12 @@
     printOnFailure(testEnv.testLogs.map((r) => r.message).join('\n'));
     expect(result, equals(0));
 
-    final [ExecutedProcess gnCall, ExecutedProcess reclientCall, ..._] = testEnv.processHistory;
+    final ExecutedProcess gnCall = testEnv.processHistory.firstWhere(
+      (p) => p.command.first.endsWith('tools/gn'),
+    );
+    final ExecutedProcess reclientCall = testEnv.processHistory.firstWhere(
+      (p) => p.command.first.endsWith('reclient/bootstrap'),
+    );
     expect(gnCall.command, containsAllInOrder([endsWith('tools/gn'), contains('--rbe')]));
     expect(reclientCall.command, containsAllInOrder([endsWith('reclient/bootstrap')]));
   });
@@ -154,8 +162,9 @@
     printOnFailure(testEnv.testLogs.map((r) => r.message).join('\n'));
     expect(result, equals(0));
 
-    print(testEnv.processHistory);
-    final [_, ExecutedProcess ninja, ..._] = testEnv.processHistory;
+    final ExecutedProcess ninja = testEnv.processHistory.firstWhere(
+      (p) => p.command.contains('-C'),
+    );
     expect(ninja.command, containsAllInOrder([endsWith('ninja/ninja'), '-j', '500']));
   });
 
@@ -268,8 +277,11 @@
     );
     final int result = await runner.run(['build', '--config', 'host_debug']);
     expect(result, equals(0));
-    expect(testEnv.processHistory[1].command[0], contains(path.join('ninja', 'ninja')));
-    expect(testEnv.processHistory[1].command[2], contains('local_host_debug'));
+    final ExecutedProcess ninja = testEnv.processHistory.firstWhere(
+      (p) => p.command.contains('-C'),
+    );
+    expect(ninja.command[0], contains(path.join('ninja', 'ninja')));
+    expect(ninja.command[2], contains('local_host_debug'));
   });
 
   test('ci config name on the command line is correctly translated', () async {
@@ -297,8 +309,11 @@
     );
     final int result = await runner.run(['build', '--config', 'ci/host_debug']);
     expect(result, equals(0));
-    expect(testEnv.processHistory[1].command[0], contains(path.join('ninja', 'ninja')));
-    expect(testEnv.processHistory[1].command[2], contains('ci/host_debug'));
+    final ExecutedProcess ninja = testEnv.processHistory.firstWhere(
+      (p) => p.command.contains('-C'),
+    );
+    expect(ninja.command[0], contains(path.join('ninja', 'ninja')));
+    expect(ninja.command[2], contains('ci/host_debug'));
   });
 
   test('build command invokes ninja with the specified target', () async {
@@ -340,7 +355,7 @@
     expect(result, equals(0));
 
     final ExecutedProcess ninjaCmd = testEnv.processHistory.firstWhere(
-      (p) => p.command.first.endsWith('ninja'),
+      (p) => p.command.first.endsWith('ninja') && p.command.contains('-C'),
     );
     expect(ninjaCmd.command, containsAllInOrder([endsWith('ninja'), '-C', endsWith('host_debug')]));
     expect(ninjaCmd.command, contains(contains('flutter/fml:fml_unittests')));
@@ -389,7 +404,7 @@
     expect(result, equals(0));
 
     final ExecutedProcess ninjaCmd = testEnv.processHistory.firstWhere(
-      (p) => p.command.first.endsWith('ninja'),
+      (p) => p.command.first.endsWith('ninja') && p.command.contains('-C'),
     );
     expect(ninjaCmd.command, containsAllInOrder([endsWith('ninja'), '-C', endsWith('host_debug')]));
 
diff --git a/engine/src/flutter/tools/engine_tool/test/commands/run_command_test.dart b/engine/src/flutter/tools/engine_tool/test/commands/run_command_test.dart
index 2d3c6c4..d1ca5bc 100644
--- a/engine/src/flutter/tools/engine_tool/test/commands/run_command_test.dart
+++ b/engine/src/flutter/tools/engine_tool/test/commands/run_command_test.dart
@@ -334,7 +334,7 @@
     test('builds only once if the target and host are the same', () async {
       await et.run(['run', '--config=host_debug']);
 
-      expect(commandsRun, containsOnce(containsAllInOrder([endsWith('ninja')])));
+      expect(commandsRun, containsOnce(containsAllInOrder([endsWith('ninja'), '-C'])));
     });
 
     test('builds both the target and host if they are different', () async {
diff --git a/engine/src/flutter/tools/pkg/engine_build_configs/lib/src/build_config_runner.dart b/engine/src/flutter/tools/pkg/engine_build_configs/lib/src/build_config_runner.dart
index b472509..d7aecb2 100644
--- a/engine/src/flutter/tools/pkg/engine_build_configs/lib/src/build_config_runner.dart
+++ b/engine/src/flutter/tools/pkg/engine_build_configs/lib/src/build_config_runner.dart
@@ -383,21 +383,56 @@
     return result.ok;
   }
 
+  /// Generates and returns the ninja compilation database for the build.
+  ///
+  /// GN's default compile_commands.json generator only exports C and C++
+  /// targets and omits custom wrapper rules like `swiftc.py`. This invokes
+  /// `ninja -t compdb` to retrieve all compilation targets in the build graph
+  /// and falls back to reading `compile_commands.json` from disk if the command
+  /// fails or returns empty.
+  Future<String?> _getCompilationDatabase() async {
+    final String ninjaPath = p.join(
+      engineSrcDir.parent.parent.path,
+      'third_party',
+      'ninja',
+      'ninja',
+    );
+    final String outDir = p.join(engineSrcDir.path, 'out', build.ninja.config);
+    final ProcessRunnerResult result = await processRunner.runProcess(
+      <String>[ninjaPath, '-t', 'compdb'],
+      workingDirectory: io.Directory(outDir),
+      failOk: true,
+    );
+    if (result.exitCode == 0 && result.stdout.isNotEmpty) {
+      return result.stdout;
+    }
+    final commandsFile = io.File(p.join(outDir, 'compile_commands.json'));
+    if (commandsFile.existsSync()) {
+      return commandsFile.readAsString();
+    }
+    return null;
+  }
+
+  /// Performs post-GN build steps.
+  ///
+  /// Retrieves the full compilation database from Ninja and processes it to
+  /// strip compiler wrapper prefixes and expand Swift compilation rules for IDE
+  /// language server compatibility.
   Future<void> _postGn() async {
     if (dryRun) {
       return;
     }
 
-    final commandsFile = io.File(
-      p.join(engineSrcDir.path, 'out', build.ninja.config, 'compile_commands.json'),
-    );
-    if (!commandsFile.existsSync()) {
+    final String? rawContents = await _getCompilationDatabase();
+    if (rawContents == null) {
       return;
     }
 
-    final String contents = await commandsFile.readAsString();
-    final String updated = stripCompilerWrappers(contents);
-    if (contents != updated) {
+    final String updated = updateCompilationDatabase(rawContents);
+    final commandsFile = io.File(
+      p.join(engineSrcDir.path, 'out', build.ninja.config, 'compile_commands.json'),
+    );
+    if (rawContents != updated || !commandsFile.existsSync()) {
       await commandsFile.writeAsString(updated);
     }
   }
diff --git a/engine/src/flutter/tools/pkg/engine_build_configs/lib/src/update_compdb.dart b/engine/src/flutter/tools/pkg/engine_build_configs/lib/src/update_compdb.dart
index 0397edd..7d79ebd 100644
--- a/engine/src/flutter/tools/pkg/engine_build_configs/lib/src/update_compdb.dart
+++ b/engine/src/flutter/tools/pkg/engine_build_configs/lib/src/update_compdb.dart
@@ -2,7 +2,25 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:convert' as convert;
+
+import 'package:path/path.dart' as p;
+
+// Matches a compile_commands.json `"command"` entry up to the clang executable.
+//
+// Matches up to `clang`/`clang++` so any wrapper prefix (rewrapper, ccache)
+// before it can be stripped.
+//
+// Group 1 is the leading `"command": "`.
+// Group 2 is the ` <path>clang[++]` executable.
+//
+// For example, given the entry:
+//   "command": "../../buildtools/mac-arm64/reclient/rewrapper ../../buildtools/mac-arm64/clang/bin/clang++ -c foo.cc"
+// group 2 is ` ../../buildtools/mac-arm64/clang/bin/clang++`.
 final RegExp _clangRegexp = RegExp(r'("command"\s*:\s*").*(\s(?:\S*/)?clang(\+\+)?)(?=[\s"])');
+final RegExp _swiftEntryRegexp = RegExp(r'\{[^{}]*swiftc\.py[^{}]*\}');
+final RegExp _shellQuoteRegexp = RegExp(r'[\s"\\$`!#&*|?()<>;~]');
+const convert.JsonEncoder _jsonEncoder = convert.JsonEncoder.withIndent('  ');
 
 /// Strips compiler wrapper prefixes from compiler commands in [contents].
 ///
@@ -16,3 +34,320 @@
     return '${match[1]}${match[2]!.trim()}';
   });
 }
+
+/// Converts GN `swiftc.py` invocations in [contents] into native `swiftc` commands.
+///
+/// Our build toolchain invokes `swiftc` from a `swiftc.py` wrapper. Language
+/// servers such as SourceKit-LSP expect expanded, per-file `swiftc` invocations
+/// in `compile_commands.json`. This replaces wrapper calls with direct
+/// invocations that language servers understand.
+String expandSwiftcCommands(String contents) {
+  if (!contents.contains('swiftc.py')) {
+    return contents;
+  }
+
+  return contents.replaceAllMapped(_swiftEntryRegexp, (Match match) {
+    final String rawJson = match.group(0)!;
+    try {
+      final entry = convert.jsonDecode(rawJson) as Map<String, Object?>;
+      final List<Map<String, Object?>> expanded = expandSwiftEntry(entry);
+      return expanded.map((Map<String, Object?> map) => _jsonEncoder.convert(map)).join(',\n  ');
+    } catch (_) {
+      // If parsing fails for any reason, leave the block untouched.
+      return rawJson;
+    }
+  });
+}
+
+/// Post-processes the contents of a `compile_commands.json` file.
+///
+/// Strips compiler wrapper prefixes (such as rewrapper and ccache) from clang
+/// commands, and converts GN `swiftc.py` invocations into native `swiftc`
+/// commands expanded per-file for SourceKit-LSP.
+String updateCompilationDatabase(String contents) {
+  contents = stripCompilerWrappers(contents);
+  return expandSwiftcCommands(contents);
+}
+
+/// Expands a single `swiftc.py` [entry] into per-file `swiftc` compilation entries.
+///
+/// If [entry] has a `command` that invokes `swiftc.py`, this parses the
+/// arguments, translates them to `swiftc` arguments, makes paths absolute
+/// against the entry's `directory`, and returns an entry for each `.swift` file
+/// compiled.
+List<Map<String, Object?>> expandSwiftEntry(Map<String, Object?> entry) {
+  final String entryDir = (entry['directory'] as String?) ?? '';
+  final Map<String, Object?> baseEntry = _absolutizeEntryFile(entry, entryDir);
+
+  final origCommand = baseEntry['command'] as String?;
+  if (origCommand == null) {
+    return [baseEntry];
+  }
+
+  final _SwiftcTranslation? translation = _translateSwiftcCommand(origCommand);
+  if (translation == null) {
+    return [baseEntry];
+  }
+
+  final translatedEntry = <String, Object?>{
+    ...baseEntry,
+    'command': _resolveCommandPaths(entryDir, translation.args),
+  };
+
+  if (translation.swiftFiles.isEmpty) {
+    return [translatedEntry];
+  }
+
+  final absSwiftFiles = <String>[
+    for (final String f in translation.swiftFiles) makePathAbsolute(entryDir, f),
+  ];
+  return _duplicateEntryPerSwiftFile(translatedEntry, absSwiftFiles);
+}
+
+/// Returns a copy of [entry] with an absolute `file` path.
+Map<String, Object?> _absolutizeEntryFile(Map<String, Object?> entry, String entryDir) {
+  final file = entry['file'] as String?;
+  return <String, Object?>{
+    ...entry,
+    if (file != null && !p.isAbsolute(file)) 'file': makePathAbsolute(entryDir, file),
+  };
+}
+
+/// Duplicates [entry] once per file in [absSwiftFiles].
+///
+/// This skips [entry]'s own `file`, which is already covered, overriding `file`
+/// on each copy, so the language server indexes every compiled Swift file.
+List<Map<String, Object?>> _duplicateEntryPerSwiftFile(
+  Map<String, Object?> entry,
+  List<String> absSwiftFiles,
+) {
+  final origFileAbs = entry['file'] as String?;
+  final results = <Map<String, Object?>>[entry];
+  for (final absFile in absSwiftFiles) {
+    if (absFile != origFileAbs) {
+      results.add(<String, Object?>{...entry, 'file': absFile});
+    }
+  }
+  return results;
+}
+
+/// Resolves [filePath] against [directory] and returns the normalized absolute path.
+String makePathAbsolute(String directory, String filePath) =>
+    p.normalize(p.isAbsolute(filePath) ? filePath : p.join(directory, filePath));
+
+/// The `swiftc` arguments and Swift source files extracted from a GN `swiftc.py` command.
+typedef _SwiftcTranslation = ({List<String> args, List<String> swiftFiles});
+
+/// Parses [cmdStr] and translates it to `swiftc` arguments.
+///
+/// Returns null if [cmdStr] is not a `swiftc.py` invocation.
+_SwiftcTranslation? _translateSwiftcCommand(String cmdStr) {
+  if (!cmdStr.contains('swiftc.py')) {
+    return null;
+  }
+  final List<String> words = splitShellWords(cmdStr);
+  final int swiftcPyIdx = words.indexWhere((String w) => w.contains('swiftc.py'));
+  if (swiftcPyIdx == -1) {
+    return null;
+  }
+  return _translateSwiftcArgs(words.sublist(swiftcPyIdx + 1));
+}
+
+/// Returns true if the flag [arg] is a boolean flag that does not take a value.
+///
+/// Most of swiftc.py's flags take a value; this returns true for those that
+/// don't.
+bool _isBooleanFlag(String arg) {
+  return arg == '--fix-generated-header';
+}
+
+/// Translates GN `swiftc.py` [args] into native `swiftc` arguments.
+///
+/// Non-path `-Xcc` flag synthesis (such as
+/// preprocessor defines `-D`) is handled here, during syntactic argument
+/// translation.
+///
+/// Path-dependent `-Xcc` flag synthesis (such as `-I` or `-F`) is deferred to
+/// [_resolveCommandPaths], where relative filesystem paths are resolved to
+/// absolute paths.
+_SwiftcTranslation _translateSwiftcArgs(List<String> args) {
+  final newArgs = <String>['swiftc', '-parse-as-library'];
+  final swiftFiles = <String>[];
+
+  var i = 0;
+  while (i < args.length) {
+    final String arg = args[i];
+    switch (arg) {
+      case '-import-objc-header':
+        // Extract header argument.
+        if (i + 1 < args.length) {
+          final String val = args[i + 1];
+          if (val.isNotEmpty && val != '""' && val != "''") {
+            newArgs.addAll(<String>['-import-objc-header', val]);
+          }
+        }
+        i += 2;
+      case '--whole-module-optimization':
+        newArgs.add('-whole-module-optimization');
+        i += 1;
+      case _ when arg.startsWith('--'):
+        i += _isBooleanFlag(arg) ? 1 : 2;
+      case '-D':
+        if (i + 1 < args.length) {
+          final String val = args[i + 1];
+          // Swift's `-D` only supports bare conditional-compilation flags, not
+          // `key=value` defines, so those are only forwarded to clang.
+          if (!val.contains('=')) {
+            newArgs.addAll(<String>['-D', val]);
+          }
+          newArgs.addAll(<String>['-Xcc', '-D$val']);
+        }
+        i += 2;
+      case _ when arg.startsWith('-D'):
+        final String val = arg.substring(2);
+        if (!val.contains('=')) {
+          newArgs.add(arg);
+        }
+        newArgs.addAll(<String>['-Xcc', '-D$val']);
+        i += 1;
+      case _:
+        newArgs.add(arg);
+        if (arg.endsWith('.swift')) {
+          swiftFiles.add(arg);
+        }
+        i += 1;
+    }
+  }
+
+  return (args: newArgs, swiftFiles: swiftFiles);
+}
+
+/// Resolves relative paths in [words] to absolute paths against [directory].
+///
+/// Synthesizes path-dependent `-Xcc` flags for include and framework search
+/// paths (`-I`, `-F`, `-isystem`, `-Fsystem`), and returns the quoted, joined
+/// command string. Non-path `-Xcc` synthesis (`-D`) is handled upstream, in
+/// [_translateSwiftcArgs].
+///
+/// `-isystem` is clang-only: it's forwarded to swift only as `-Xcc -isystem
+/// -Xcc <path>`, never as a bare swift-side flag.
+String _resolveCommandPaths(String directory, List<String> words) {
+  final newArgs = <String>[];
+
+  // `-isystem` is the only one of these forwarded to clang exclusively; see
+  // the doc comment above.
+  bool isClangOnly(String flag) => flag == '-isystem';
+
+  int addSeparatedIncludeFlag(int i) {
+    final String flag = words[i];
+    if (!isClangOnly(flag)) {
+      newArgs.add(flag);
+    }
+    if (i + 1 >= words.length) {
+      return i + 1;
+    }
+    final String absVal = makePathAbsolute(directory, words[i + 1]);
+    if (!isClangOnly(flag)) {
+      newArgs.add(absVal);
+    }
+    if (flag case '-I' || '-isystem' || '-F' || '-Fsystem') {
+      newArgs.addAll(<String>['-Xcc', flag, '-Xcc', absVal]);
+    }
+    return i + 2;
+  }
+
+  void addAttachedIncludeFlag(String arg) {
+    final String prefix = switch (arg) {
+      _ when arg.startsWith('-isystem') => '-isystem',
+      _ when arg.startsWith('-Fsystem') => '-Fsystem',
+      _ when arg.startsWith('-F') => '-F',
+      _ => '-I',
+    };
+
+    // A bare `-I`/`-isystem`/`-F`/`-Fsystem` (no attached value) is caught by
+    // the exact-match branch below instead, so `val` is never empty here.
+    final String val = makePathAbsolute(directory, arg.substring(prefix.length));
+    if (!isClangOnly(prefix)) {
+      newArgs.add('$prefix$val');
+    }
+    newArgs.addAll(<String>['-Xcc', '$prefix$val']);
+  }
+
+  var i = 0;
+  while (i < words.length) {
+    final String arg = words[i];
+    switch (arg) {
+      case _ when arg.endsWith('.swift'):
+        newArgs.add(makePathAbsolute(directory, arg));
+        i += 1;
+      case '-I' || '-isystem' || '-F' || '-Fsystem' || '-import-objc-header' || '-sdk':
+        i = addSeparatedIncludeFlag(i);
+      case _ when arg.startsWith('-I') || arg.startsWith('-F') || arg.startsWith('-isystem'):
+        addAttachedIncludeFlag(arg);
+        i += 1;
+      case _:
+        newArgs.add(arg);
+        i += 1;
+    }
+  }
+  return newArgs.map(quoteShellWord).join(' ');
+}
+
+/// Parses [cmd] into individual shell arguments.
+///
+/// An empty quoted argument (`""` or `''`) is preserved as an empty-string
+/// element rather than disappearing, so callers can distinguish "argument
+/// present but empty" from "argument absent".
+List<String> splitShellWords(String cmd) {
+  final args = <String>[];
+  final buffer = StringBuffer();
+  var inSingleQuote = false;
+  var inDoubleQuote = false;
+  var escape = false;
+  var quoted = false;
+
+  for (var i = 0; i < cmd.length; i += 1) {
+    final String char = cmd[i];
+    switch (char) {
+      case _ when escape:
+        buffer.write(char);
+        escape = false;
+      case r'\' when !inSingleQuote:
+        escape = true;
+      case "'" when !inDoubleQuote:
+        inSingleQuote = !inSingleQuote;
+        quoted = true;
+      case '"' when !inSingleQuote:
+        inDoubleQuote = !inDoubleQuote;
+        quoted = true;
+      case ' ' || '\t' when !inSingleQuote && !inDoubleQuote:
+        if (buffer.isNotEmpty || quoted) {
+          args.add(buffer.toString());
+          buffer.clear();
+          quoted = false;
+        }
+      case _:
+        buffer.write(char);
+    }
+  }
+  if (buffer.isNotEmpty || quoted) {
+    args.add(buffer.toString());
+  }
+  return args;
+}
+
+/// Quotes and escapes [arg] for safe inclusion as a shell argument.
+String quoteShellWord(String arg) {
+  if (arg.isEmpty) {
+    return "''";
+  }
+  if (!arg.contains(_shellQuoteRegexp)) {
+    return arg;
+  }
+  if (!arg.contains("'")) {
+    return "'$arg'";
+  }
+  return r'"'
+      '${arg.replaceAll(r'\', r'\\').replaceAll('"', r'\"')}'
+      r'"';
+}
diff --git a/engine/src/flutter/tools/pkg/engine_build_configs/test/update_compdb_test.dart b/engine/src/flutter/tools/pkg/engine_build_configs/test/update_compdb_test.dart
index 81678fc..96df159 100644
--- a/engine/src/flutter/tools/pkg/engine_build_configs/test/update_compdb_test.dart
+++ b/engine/src/flutter/tools/pkg/engine_build_configs/test/update_compdb_test.dart
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:convert' as convert;
+
 import 'package:engine_build_configs/src/update_compdb.dart';
 import 'package:test/test.dart';
 
@@ -26,6 +28,7 @@
   }
 ]
 ''';
+      expect(updateCompilationDatabase(input), equals(expected));
       expect(stripCompilerWrappers(input), equals(expected));
     });
 
@@ -48,6 +51,7 @@
   }
 ]
 ''';
+      expect(updateCompilationDatabase(input), equals(expected));
       expect(stripCompilerWrappers(input), equals(expected));
     });
 
@@ -70,6 +74,7 @@
   }
 ]
 ''';
+      expect(updateCompilationDatabase(input), equals(expected));
       expect(stripCompilerWrappers(input), equals(expected));
     });
 
@@ -83,7 +88,220 @@
   }
 ]
 ''';
+      expect(updateCompilationDatabase(input), equals(input));
       expect(stripCompilerWrappers(input), equals(input));
     });
   });
+
+  group('expandSwiftcCommands', () {
+    test('leaves standard compile_commands untouched if no swiftc.py or wrapper is present', () {
+      const input = r'''
+[
+  {
+    "file": "../../flutter/foo.cc",
+    "directory": "/out/config",
+    "command": "../../clang/bin/clang++ -c ../../flutter/foo.cc"
+  }
+]
+''';
+      expect(updateCompilationDatabase(input), equals(input));
+      expect(stripCompilerWrappers(input), equals(input));
+      expect(expandSwiftcCommands(input), equals(input));
+    });
+
+    test('translates swiftc.py flags and makes paths absolute', () {
+      const input = r'''
+[
+  {
+    "file": "../../flutter/bar.swift",
+    "directory": "/out/config",
+    "command": "python3 ../../flutter/tools/swiftc.py -module-name Bar -import-objc-header ../../flutter/header.h -target arm64-macos14.0 -I ../../flutter/include -D FOO_BAR ../../flutter/bar.swift"
+  }
+]
+''';
+      final String output = updateCompilationDatabase(input);
+      final json = convert.jsonDecode(output) as List<dynamic>;
+      expect(json.length, equals(1));
+      final entry = json[0] as Map<String, dynamic>;
+      expect(entry['file'], equals('/flutter/bar.swift'));
+      final command = entry['command'] as String;
+      expect(command, contains('swiftc'));
+      expect(command, contains('-parse-as-library'));
+      expect(command, contains('-module-name Bar'));
+      expect(command, contains('-import-objc-header /flutter/header.h'));
+      expect(command, contains('-target arm64-macos14.0'));
+      expect(command, contains('-I /flutter/include'));
+      expect(command, contains('-Xcc -I -Xcc /flutter/include'));
+      expect(command, contains('-D FOO_BAR'));
+      expect(command, contains('-Xcc -DFOO_BAR'));
+    });
+
+    test('-isystem is forwarded to clang only, never bare to swiftc', () {
+      const input = r'''
+[
+  {
+    "file": "../../flutter/bar.swift",
+    "directory": "/out/config",
+    "command": "python3 ../../flutter/tools/swiftc.py -module-name Bar -isystem ../../flutter/sysinclude ../../flutter/bar.swift"
+  }
+]
+''';
+      final String output = updateCompilationDatabase(input);
+      final json = convert.jsonDecode(output) as List<dynamic>;
+      final entry = json[0] as Map<String, dynamic>;
+      final List<String> args = splitShellWords(entry['command'] as String);
+      expect(
+        args,
+        equals(<String>[
+          'swiftc',
+          '-parse-as-library',
+          '-module-name',
+          'Bar',
+          '-Xcc',
+          '-isystem',
+          '-Xcc',
+          '/flutter/sysinclude',
+          '/flutter/bar.swift',
+        ]),
+      );
+    });
+
+    test('-F and -Fsystem are forwarded to both swiftc and clang', () {
+      const input = r'''
+[
+  {
+    "file": "../../flutter/bar.swift",
+    "directory": "/out/config",
+    "command": "python3 ../../flutter/tools/swiftc.py -module-name Bar -F ../../flutter/frameworks -Fsystem../../flutter/sysframeworks ../../flutter/bar.swift"
+  }
+]
+''';
+      final String output = updateCompilationDatabase(input);
+      final json = convert.jsonDecode(output) as List<dynamic>;
+      final entry = json[0] as Map<String, dynamic>;
+      final List<String> args = splitShellWords(entry['command'] as String);
+      expect(
+        args,
+        equals(<String>[
+          'swiftc',
+          '-parse-as-library',
+          '-module-name',
+          'Bar',
+          '-F',
+          '/flutter/frameworks',
+          '-Xcc',
+          '-F',
+          '-Xcc',
+          '/flutter/frameworks',
+          '-Fsystem/flutter/sysframeworks',
+          '-Xcc',
+          '-Fsystem/flutter/sysframeworks',
+          '/flutter/bar.swift',
+        ]),
+      );
+    });
+
+    test('-D with a value ("key=value") is forwarded to clang only', () {
+      const input = r'''
+[
+  {
+    "file": "../../flutter/bar.swift",
+    "directory": "/out/config",
+    "command": "python3 ../../flutter/tools/swiftc.py -module-name Bar -D FOO=1 -DBAR=2 ../../flutter/bar.swift"
+  }
+]
+''';
+      final String output = updateCompilationDatabase(input);
+      final json = convert.jsonDecode(output) as List<dynamic>;
+      final entry = json[0] as Map<String, dynamic>;
+      final List<String> args = splitShellWords(entry['command'] as String);
+      expect(
+        args,
+        equals(<String>[
+          'swiftc',
+          '-parse-as-library',
+          '-module-name',
+          'Bar',
+          '-Xcc',
+          '-DFOO=1',
+          '-Xcc',
+          '-DBAR=2',
+          '/flutter/bar.swift',
+        ]),
+      );
+    });
+
+    test('drops -import-objc-header when the value is an empty quoted string', () {
+      const input = r'''
+[
+  {
+    "file": "../../flutter/bar.swift",
+    "directory": "/out/config",
+    "command": "python3 ../../flutter/tools/swiftc.py -module-name Bar -import-objc-header \"\" -target arm64-macos14.0 ../../flutter/bar.swift"
+  }
+]
+''';
+      final String output = updateCompilationDatabase(input);
+      final json = convert.jsonDecode(output) as List<dynamic>;
+      final entry = json[0] as Map<String, dynamic>;
+      final command = entry['command'] as String;
+      expect(command, isNot(contains('-import-objc-header')));
+      expect(command, contains('-module-name Bar'));
+      expect(command, contains('-target arm64-macos14.0'));
+    });
+
+    test('leaves a malformed swiftc.py JSON block untouched', () {
+      const input = r'''
+[
+  {
+    "file": "../../flutter/bar.swift"
+    "directory": "/out/config",
+    "command": "python3 ../../flutter/tools/swiftc.py -module-name Bar ../../flutter/bar.swift"
+  }
+]
+''';
+      expect(expandSwiftcCommands(input), equals(input));
+    });
+
+    test('expands multi-file swiftc.py invocations into individual entries', () {
+      const input = r'''
+[
+  {
+    "file": "../../flutter/bar.swift",
+    "directory": "/out/config",
+    "command": "python3 ../../flutter/tools/swiftc.py -module-name Bar ../../flutter/bar.swift ../../flutter/baz.swift"
+  }
+]
+''';
+      final String output = updateCompilationDatabase(input);
+      final json = convert.jsonDecode(output) as List<dynamic>;
+      expect(json.length, equals(2));
+      final entry1 = json[0] as Map<String, dynamic>;
+      final entry2 = json[1] as Map<String, dynamic>;
+      expect(entry1['file'], equals('/flutter/bar.swift'));
+      expect(entry2['file'], equals('/flutter/baz.swift'));
+      expect(entry1['command'], equals(entry2['command']));
+    });
+  });
+
+  group('splitShellWords / quoteShellWord', () {
+    test('splitShellWords and quoteShellWord work correctly', () {
+      final List<String> words = splitShellWords("python3 'foo bar' \"baz qux\" -D \"\\\$FOO\"");
+      expect(words, equals(<String>['python3', 'foo bar', 'baz qux', '-D', r'$FOO']));
+      expect(quoteShellWord('foo bar'), equals("'foo bar'"));
+      expect(quoteShellWord('simple'), equals('simple'));
+    });
+
+    test('splitShellWords handles edge cases gracefully', () {
+      expect(splitShellWords(''), isEmpty);
+      expect(splitShellWords('   \t  '), isEmpty);
+      expect(splitShellWords(r'foo\'), equals(<String>['foo']));
+      expect(quoteShellWord(''), equals("''"));
+    });
+
+    test('splitShellWords preserves an empty quoted argument as an empty string', () {
+      expect(splitShellWords('-foo "" -bar'), equals(<String>['-foo', '', '-bar']));
+      expect(splitShellWords("-foo '' -bar"), equals(<String>['-foo', '', '-bar']));
+    });
+  });
 }
diff --git a/engine/src/flutter/tools/vscode_workspace/engine-workspace.yaml b/engine/src/flutter/tools/vscode_workspace/engine-workspace.yaml
index e70e350..9e79404 100644
--- a/engine/src/flutter/tools/vscode_workspace/engine-workspace.yaml
+++ b/engine/src/flutter/tools/vscode_workspace/engine-workspace.yaml
@@ -129,6 +129,8 @@
     - ${workspaceFolder}
   dotnet.defaultSolution: disable
   dart.showTodos: false
+  swift.sourcekit-lsp.supported-languages:
+    - swift
   testMate.cpp.test.advancedExecutables:
     - name: impeller_unittests_arm64
       pattern: ../out/host_debug_unopt_arm64/impeller_unittests