diff --git a/.github/workflows/job-compile-and-test.yml b/.github/workflows/job-compile-and-test.yml
index fa716fd77..c5d2392a6 100644
--- a/.github/workflows/job-compile-and-test.yml
+++ b/.github/workflows/job-compile-and-test.yml
@@ -106,6 +106,11 @@ jobs:
run: yarn test --scenario=SimpleCppProject
working-directory: Extension
+ - name: Run CppModulesProject tests (Windows)
+ if: ${{ inputs.platform == 'windows' }}
+ run: yarn test --scenario=CppModulesProject
+ working-directory: Extension
+
- name: Run E2E IntelliSense features tests (Windows)
if: ${{ inputs.platform == 'windows' }}
run: yarn test --scenario=MultirootDeadlockTest
diff --git a/Extension/test/scenarios/CppModulesProject/assets/.gitignore b/Extension/test/scenarios/CppModulesProject/assets/.gitignore
new file mode 100644
index 000000000..47001bd44
--- /dev/null
+++ b/Extension/test/scenarios/CppModulesProject/assets/.gitignore
@@ -0,0 +1,3 @@
+# Generated at runtime by the scenario tests.
+.vscode/
+build/
diff --git a/Extension/test/scenarios/CppModulesProject/assets/bin/gcc b/Extension/test/scenarios/CppModulesProject/assets/bin/gcc
new file mode 100644
index 000000000..e69de29bb
diff --git a/Extension/test/scenarios/CppModulesProject/assets/bin/gcc.exe b/Extension/test/scenarios/CppModulesProject/assets/bin/gcc.exe
new file mode 100644
index 000000000..e69de29bb
diff --git a/Extension/test/scenarios/CppModulesProject/assets/main.cpp b/Extension/test/scenarios/CppModulesProject/assets/main.cpp
new file mode 100644
index 000000000..4f3b8a6c3
--- /dev/null
+++ b/Extension/test/scenarios/CppModulesProject/assets/main.cpp
@@ -0,0 +1,27 @@
+// Module importer used by the compile_commands.json based tests.
+//
+// Diagnostics on the import statements are expected until IntelliSense
+// support for C++20 modules is available for all compilers
+// (https://github.com/microsoft/vscode-cpptools/issues/6302).
+
+import math;
+import geometry;
+
+// Sentinel probe: this define is only provided by the compile_commands.json
+// entry for this file. The test hovers on the use of the symbol below to
+// verify that the compilation database entry -- including its module related
+// flags -- was actually applied to this translation unit, rather than
+// IntelliSense falling back to a default configuration.
+#ifdef CPPMODULES_COMPILE_COMMANDS_SENTINEL
+int compile_database_sentinel_value = 1;
+#endif
+
+int probe_compile_database()
+{
+ return compile_database_sentinel_value;
+}
+
+int main()
+{
+ return add(1, 2) + square_area(3) - math::k_answer;
+}
diff --git a/Extension/test/scenarios/CppModulesProject/assets/main2.cpp b/Extension/test/scenarios/CppModulesProject/assets/main2.cpp
new file mode 100644
index 000000000..950841de5
--- /dev/null
+++ b/Extension/test/scenarios/CppModulesProject/assets/main2.cpp
@@ -0,0 +1,23 @@
+// Module importer used by the custom configuration provider tests
+// (GCC-style module compiler arguments).
+
+import math;
+import geometry;
+
+// Sentinel probe: this define is only provided through the compilerArgs of the
+// custom configuration provider used by the tests. The hover assertion proves
+// that the provider's arguments (which also carry the GCC module flags) were
+// delivered to the language server for this translation unit.
+#ifdef PROVIDER_GCC_SENTINEL
+int provider_gcc_sentinel_value = 1;
+#endif
+
+int probe_provider_gcc()
+{
+ return provider_gcc_sentinel_value;
+}
+
+int main()
+{
+ return add(2, 3) + square_area(4) - math::k_answer;
+}
diff --git a/Extension/test/scenarios/CppModulesProject/assets/main3.cpp b/Extension/test/scenarios/CppModulesProject/assets/main3.cpp
new file mode 100644
index 000000000..ca0de74f1
--- /dev/null
+++ b/Extension/test/scenarios/CppModulesProject/assets/main3.cpp
@@ -0,0 +1,23 @@
+// Module importer used by the custom configuration provider tests
+// (MSVC-style module compiler arguments).
+
+import math;
+import geometry;
+
+// Sentinel probe: this define is only provided through the compilerArgs of the
+// custom configuration provider used by the tests. The hover assertion proves
+// that the provider's arguments (which also carry the MSVC module arguments)
+// were delivered to the language server for this translation unit.
+#ifdef PROVIDER_MSVC_SENTINEL
+int provider_msvc_sentinel_value = 1;
+#endif
+
+int probe_provider_msvc()
+{
+ return provider_msvc_sentinel_value;
+}
+
+int main()
+{
+ return add(3, 4) + square_area(5) - math::k_answer;
+}
diff --git a/Extension/test/scenarios/CppModulesProject/assets/math.cppm b/Extension/test/scenarios/CppModulesProject/assets/math.cppm
new file mode 100644
index 000000000..c64f69b97
--- /dev/null
+++ b/Extension/test/scenarios/CppModulesProject/assets/math.cppm
@@ -0,0 +1,24 @@
+// Module interface unit using the GCC-style file extension (.cppm).
+//
+// IntelliSense support for C++20 modules is tracked at
+// https://github.com/microsoft/vscode-cpptools/issues/6302. These sources are
+// used by the CppModulesProject scenario tests to cover the extension-side
+// behavior that module based projects rely on.
+
+export module math;
+
+export int add(int a, int b)
+{
+ return a + b;
+}
+
+export namespace math
+{
+ constexpr int k_answer = 42;
+}
+
+// Not exported; part of the module implementation only.
+int implementation_detail()
+{
+ return math::k_answer;
+}
diff --git a/Extension/test/scenarios/CppModulesProject/assets/math.ixx b/Extension/test/scenarios/CppModulesProject/assets/math.ixx
new file mode 100644
index 000000000..66f122575
--- /dev/null
+++ b/Extension/test/scenarios/CppModulesProject/assets/math.ixx
@@ -0,0 +1,13 @@
+// Module interface unit using the MSVC-style file extension (.ixx).
+//
+// IntelliSense support for C++20 modules is tracked at
+// https://github.com/microsoft/vscode-cpptools/issues/6302. These sources are
+// used by the CppModulesProject scenario tests to cover the extension-side
+// behavior that module based projects rely on.
+
+export module geometry;
+
+export int square_area(int side)
+{
+ return side * side;
+}
diff --git a/Extension/test/scenarios/CppModulesProject/tests/modules.integration.test.ts b/Extension/test/scenarios/CppModulesProject/tests/modules.integration.test.ts
new file mode 100644
index 000000000..8702cefe7
--- /dev/null
+++ b/Extension/test/scenarios/CppModulesProject/tests/modules.integration.test.ts
@@ -0,0 +1,361 @@
+/* --------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All Rights Reserved.
+ * See 'LICENSE' in the project root for license information.
+ * ------------------------------------------------------------------------------------------ */
+
+/* eslint-disable @typescript-eslint/triple-slash-reference */
+///
+
+// Groundwork for IntelliSense support of C++20 modules importing
+// (https://github.com/microsoft/vscode-cpptools/issues/6302).
+//
+// These tests cover the extension-side behavior that module based projects
+// rely on today:
+// * module interface files (.cppm/.ixx) are recognized as C++ sources,
+// * a compile_commands.json that contains GCC module related flags does not
+// prevent the language server from providing IntelliSense for the project,
+// and its entries are actually applied to the translation units (verified
+// through a sentinel define and a hover assertion, not just readiness),
+// * module related compiler arguments (both GCC and MSVC spellings) are
+// accepted and delivered through the custom configuration provider path,
+// verified the same way.
+//
+// No statement is made about the semantic understanding of module imports by
+// the language server itself: that support is still in development and is
+// tracked by the issue referenced above. When it ships, these tests are the
+// place to add end-to-end module IntelliSense assertions.
+
+import * as assert from 'assert';
+import { suite } from 'mocha';
+import * as path from 'path';
+import * as vscode from 'vscode';
+import * as api from 'vscode-cpptools';
+import * as apit from 'vscode-cpptools/out/testApi';
+import * as config from '../../../../src/LanguageServer/configurations';
+import * as util from '../../../../src/common';
+import * as testHelpers from '../../../common/testHelpers';
+
+/* **************************************************************************** */
+
+function workspacePath(): string {
+ const folder = vscode.workspace.workspaceFolders?.[0];
+ assert.ok(folder, 'workspace folder should be set.');
+
+ return folder.uri.fsPath;
+}
+
+function toPosixPath(path: string): string {
+ return path.replace(/\\/g, '/');
+}
+
+// The (empty) fake gcc binaries are checked in with the scenario assets,
+// following the SimpleCppProject pattern, so that the language server does not
+// probe a real compiler and the tests stay deterministic across machines.
+function fakeGccPath(): string {
+ return toPosixPath(`${workspacePath()}/bin/${process.platform === 'win32' ? 'gcc.exe' : 'gcc'}`);
+}
+
+// The directories of these files are created on demand by util.writeFileText,
+// but only when the path uses native separators, hence path.join here. The
+// contents written to the JSON files use posix paths (see toPosixPath).
+function compileCommandsPath(): string {
+ return path.join(workspacePath(), 'build', 'compile_commands.json');
+}
+
+function cppPropertiesPath(): string {
+ return path.join(workspacePath(), '.vscode', 'c_cpp_properties.json');
+}
+
+async function changeCppProperties(cppProperties: config.ConfigurationJson, _disposables: vscode.Disposable[]): Promise {
+ await util.writeFileText(cppPropertiesPath(), JSON.stringify(cppProperties));
+ const contents: string = await util.readFileText(cppPropertiesPath());
+ console.log(" wrote c_cpp_properties.json: " + contents);
+
+ // Sleep for 4000ms for file watcher
+ return new Promise(r => setTimeout(r, 4000));
+}
+
+async function openAndAwaitIntelliSenseReady(cpptools: apit.CppToolsTestApi, disposables: vscode.Disposable[], filename: string): Promise {
+ const filePath: string = workspacePath() + "/" + filename;
+
+ const testHook: apit.CppToolsTestHook = cpptools.getTestHook();
+ const testResult: Promise = new Promise((resolve, reject) => {
+ disposables.push(testHook.IntelliSenseStatusChanged(result => {
+ result = result as apit.IntelliSenseStatus;
+ if (result.filename === filename && result.status === apit.Status.IntelliSenseReady) {
+ resolve();
+ }
+ }));
+ setTimeout(() => { reject(new Error(`timeout waiting for IntelliSenseReady: ${filename}`)); }, testHelpers.defaultTimeout);
+ });
+
+ const document: vscode.TextDocument = await vscode.workspace.openTextDocument(filePath);
+ await vscode.window.showTextDocument(document);
+ await testResult;
+ return document;
+}
+
+// Verifies, through IntelliSense, that a sentinel define reached the language
+// server for the given document: hovering over a use of a symbol that is only
+// declared under "#ifdef " resolves if and only if the define was
+// applied to the translation unit. This observes the *effect* of the delivered
+// configuration instead of merely re-checking what a provider returned.
+async function assertSentinelDefineApplied(document: vscode.TextDocument, useStatement: string, symbol: string): Promise {
+ const text: string = document.getText();
+ const useIndex: number = text.indexOf(useStatement);
+ assert.ok(useIndex >= 0, `use statement not found in document: ${useStatement}`);
+ const symbolOffsetInUse: number = useStatement.indexOf(symbol);
+ assert.ok(symbolOffsetInUse >= 0, `symbol not found in use statement: ${symbol}`);
+
+ // +1 to hover from inside the token.
+ const position: vscode.Position = document.positionAt(useIndex + symbolOffsetInUse + 1);
+
+ // The hover can lag slightly behind IntelliSenseReady; retry for a while.
+ const deadline: number = Date.now() + testHelpers.defaultTimeout;
+ while (Date.now() < deadline) {
+ const hovers: vscode.Hover[] = await vscode.commands.executeCommand('vscode.executeHoverProvider', document.uri, position);
+ if (hovers.length > 0) {
+ return;
+ }
+ await testHelpers.delay(500);
+ }
+ assert.fail(`hover did not resolve '${symbol}' -- the sentinel define was not applied to ${document.uri.fsPath}`);
+}
+
+/* **************************************************************************** */
+
+interface CompileCommandEntry {
+ directory: string;
+ arguments: string[];
+ file: string;
+}
+
+// GCC spellings of the C++20 module related compiler flags, as emitted by
+// CMake's C++ module support for GCC targets.
+function gccModuleArgs(cmiDir: string): string[] {
+ return [
+ "-std=c++20",
+ "-fmodules-ts",
+ `-fprebuilt-module-path=${cmiDir}`,
+ `-fmodule-file=math=${cmiDir}/math.gcm`
+ ];
+}
+
+function compileCommandEntry(file: string, extraArgs: string[] = []): CompileCommandEntry {
+ const root: string = toPosixPath(workspacePath());
+ const objectFile: string = `${root}/build/${file.replace(/\.[^.]+$/, "")}.o`;
+ return {
+ directory: root,
+ arguments: [fakeGccPath(), ...gccModuleArgs(`${root}/build/cmi`), ...extraArgs, "-c", "-o", objectFile, file],
+ file: file
+ };
+}
+
+async function writeCompileCommands(entries: CompileCommandEntry[]): Promise {
+ await util.writeFileText(compileCommandsPath(), JSON.stringify(entries, null, 2));
+ console.log(" wrote build/compile_commands.json for: " + entries.map(entry => entry.file).join(", "));
+}
+
+/* **************************************************************************** */
+
+// MSVC spellings of the C++20 module related compiler arguments, as used by
+// the (currently MSVC-only) modules IntelliSense workaround.
+function msvcModuleArgs(): string[] {
+ const ifcDir: string = `${toPosixPath(workspacePath())}/build/ifc`;
+ return [
+ "/ifcSearchDir",
+ ifcDir,
+ "/reference",
+ `math=${ifcDir}/math.ifc`,
+ "/headerUnit",
+ `std=${ifcDir}/std.ifc`,
+ "/stdIfcDir",
+ ifcDir
+ ];
+}
+
+/* **************************************************************************** */
+
+suite("C++20 modules file type tests", function(): void {
+ suiteSetup(async function(): Promise {
+ await testHelpers.activateCppExtension();
+ });
+
+ test("Module interface files are opened as C++ (.cppm)", async () => {
+ const document: vscode.TextDocument = await vscode.workspace.openTextDocument(workspacePath() + "/math.cppm");
+ await vscode.window.showTextDocument(document);
+ assert.strictEqual(document.languageId, 'cpp');
+ });
+
+ test("Module interface files are opened as C++ (.ixx)", async () => {
+ const document: vscode.TextDocument = await vscode.workspace.openTextDocument(workspacePath() + "/math.ixx");
+ await vscode.window.showTextDocument(document);
+ assert.strictEqual(document.languageId, 'cpp');
+ });
+});
+
+/* **************************************************************************** */
+
+suite("compile_commands.json with GCC C++20 module flags", function(): void {
+ let cpptools: apit.CppToolsTestApi;
+ const disposables: vscode.Disposable[] = [];
+
+ suiteSetup(async function(): Promise {
+ cpptools = await apit.getCppToolsTestApi(api.Version.v2) ?? assert.fail('Could not get CppToolsTestApi');
+ disposables.push(cpptools);
+
+ await writeCompileCommands([
+ // The sentinel define carried by main.cpp's entry lets the test
+ // verify through IntelliSense (hover) that the database entry was
+ // applied, rather than IntelliSense falling back to a default
+ // configuration.
+ compileCommandEntry("main.cpp", ["-DCPPMODULES_COMPILE_COMMANDS_SENTINEL=1"]),
+ compileCommandEntry("math.cppm"),
+ compileCommandEntry("math.ixx")
+ ]);
+
+ await changeCppProperties({
+ configurations: [
+ {
+ name: "GCC C++20 modules",
+ compileCommands: [compileCommandsPath()],
+ intelliSenseMode: "gcc-x64",
+ cppStandard: "c++20"
+ }
+ ],
+ version: 4
+ },
+ disposables);
+ });
+
+ suiteTeardown(function(): void {
+ disposables.forEach(d => d.dispose());
+ });
+
+ test("IntelliSense is provided for the module importer (main.cpp)", async () => {
+ const document: vscode.TextDocument = await openAndAwaitIntelliSenseReady(cpptools, disposables, "main.cpp");
+
+ // Verify that the compile_commands.json entry for this file -- the one
+ // carrying the module related flags -- was actually applied to the
+ // translation unit (and not a fallback configuration).
+ await assertSentinelDefineApplied(document, "return compile_database_sentinel_value;", "compile_database_sentinel_value");
+ });
+});
+
+/* **************************************************************************** */
+
+suite("custom configuration provider with module compiler args", function(): void {
+ let cpptools: apit.CppToolsTestApi;
+ let lastResult: api.SourceFileConfigurationItem[];
+ let configurationProvidedBeforeCanProvide: boolean = false;
+ const supportedUris: Set = new Set();
+ const disposables: vscode.Disposable[] = [];
+
+ // gcc-style module args for main2.cpp, MSVC-style module args for main3.cpp.
+ // Each config also carries a sentinel define -- through the provider API's
+ // `defines` channel, which is the documented way to deliver defines -- so
+ // the tests can verify through IntelliSense (hover) that the configuration
+ // was actually delivered and applied for the translation unit, not merely
+ // returned by the provider. The module flags stay in `compilerArgs`.
+ const gccModuleConfig: api.SourceFileConfiguration = {
+ includePath: [ "${workspaceFolder}" ],
+ defines: [ "PROVIDER_GCC_SENTINEL=1" ],
+ intelliSenseMode: "gcc-x64",
+ standard: "c++20",
+ compilerArgs: gccModuleArgs(`${toPosixPath(workspacePath())}/build/cmi`)
+ };
+ const msvcModuleConfig: api.SourceFileConfiguration = {
+ includePath: [ "${workspaceFolder}" ],
+ defines: [ "PROVIDER_MSVC_SENTINEL=1" ],
+ intelliSenseMode: "msvc-x64",
+ standard: "c++20",
+ compilerArgs: msvcModuleArgs()
+ };
+
+ const provider: api.CustomConfigurationProvider = {
+ name: "cpptoolsTest-modules",
+ extensionId: "ms-vscode.cpptools-test-modules",
+ canProvideConfiguration(document: vscode.Uri): Thenable {
+ supportedUris.add(document.toString());
+ return Promise.resolve(true);
+ },
+ provideConfigurations(uris: vscode.Uri[]): Thenable {
+ const result: api.SourceFileConfigurationItem[] = [];
+ uris.forEach(uri => {
+ const uriString: string = uri.toString();
+ if (!supportedUris.has(uriString)) {
+ configurationProvidedBeforeCanProvide = true;
+ }
+ const configuration: api.SourceFileConfiguration = uri.path.endsWith("main3.cpp") ? msvcModuleConfig : gccModuleConfig;
+ result.push({
+ uri: uriString,
+ configuration: configuration
+ });
+ });
+ lastResult = result;
+ return Promise.resolve(result);
+ },
+ dispose(): void {
+ console.log(" disposed");
+ },
+ canProvideBrowseConfiguration(): Thenable {
+ return Promise.resolve(false);
+ },
+ provideBrowseConfiguration(): Thenable {
+ return Promise.resolve({ browsePath: [] });
+ },
+ canProvideBrowseConfigurationsPerFolder(): Thenable {
+ return Promise.resolve(false);
+ },
+ provideFolderBrowseConfiguration(_uri: vscode.Uri): Thenable {
+ return Promise.resolve({ browsePath: [] });
+ }
+ };
+
+ suiteSetup(async function(): Promise {
+ cpptools = await apit.getCppToolsTestApi(api.Version.v6) ?? assert.fail('Could not get CppToolsTestApi');
+ cpptools.registerCustomConfigurationProvider(provider);
+ cpptools.notifyReady(provider);
+ disposables.push(cpptools);
+
+ await changeCppProperties({
+ configurations: [ { name: "modules-provider", configurationProvider: provider.extensionId } ],
+ version: 4
+ },
+ disposables);
+ });
+
+ suiteTeardown(async function(): Promise {
+ disposables.forEach(d => d.dispose());
+ await util.deleteFile(cppPropertiesPath());
+ await util.deleteFile(compileCommandsPath());
+ });
+
+ test("GCC-style module compiler args are delivered - main2.cpp", async () => {
+ const document: vscode.TextDocument = await openAndAwaitIntelliSenseReady(cpptools, disposables, "main2.cpp");
+ // Handshake check: the extension asked before configurations were
+ // provided, and items were delivered for this file. (The objects
+ // themselves are intentionally not re-compared against the provider's
+ // own return value; the assertions below observe the delivered
+ // configuration through its effect on IntelliSense instead.)
+ assert.strictEqual(configurationProvidedBeforeCanProvide, false);
+ assert.deepStrictEqual(lastResult.map(item => item.uri), [document.uri.toString()]);
+
+ // Verify that the configuration -- whose compilerArgs carry the GCC
+ // module flags -- was actually delivered and applied for this
+ // translation unit.
+ await assertSentinelDefineApplied(document, "return provider_gcc_sentinel_value;", "provider_gcc_sentinel_value");
+ });
+
+ test("MSVC-style module compiler args are delivered - main3.cpp", async () => {
+ const document: vscode.TextDocument = await openAndAwaitIntelliSenseReady(cpptools, disposables, "main3.cpp");
+ // Handshake check, as above.
+ assert.strictEqual(configurationProvidedBeforeCanProvide, false);
+ assert.deepStrictEqual(lastResult.map(item => item.uri), [document.uri.toString()]);
+
+ // Verify that the configuration -- whose compilerArgs carry the MSVC
+ // module arguments -- was actually delivered and applied for this
+ // translation unit.
+ await assertSentinelDefineApplied(document, "return provider_msvc_sentinel_value;", "provider_msvc_sentinel_value");
+ });
+});