diff --git a/package.json b/package.json index 21af60a52..8abfae5db 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "workspaceContains:**/compile_commands.json", "workspaceContains:**/compile_flags.txt", "workspaceContains:**/buildServer.json", + "workspaceContains:**/.bsp/*.json", "onDebugResolve:swift-lldb", "onDebugResolve:swift" ], @@ -544,6 +545,22 @@ "markdownDescription": "Search sub-folders of workspace folder for Swift Packages at start up.", "scope": "machine-overridable" }, + "swift.ignoreSearchingForPackagesInSubfolders": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + ".", + ".build", + "Packages", + "out", + "bazel-out", + "bazel-bin" + ], + "markdownDescription": "A list of glob patterns to ignore when searching sub-folders for Swift Packages. The `swift.searchSubfoldersForPackages` must be `true` for this setting to have an effect. Always use forward-slashes in glob expressions regardless of platform. This is combined with VS Code's default `files.exclude` setting.", + "scope": "machine-overridable" + }, "swift.autoGenerateLaunchConfigurations": { "type": "boolean", "default": true, diff --git a/src/WorkspaceContext.ts b/src/WorkspaceContext.ts index 68403a4f0..3448f99bb 100644 --- a/src/WorkspaceContext.ts +++ b/src/WorkspaceContext.ts @@ -448,6 +448,7 @@ export class WorkspaceContext implements vscode.Disposable { workspaceFolder.uri, configuration.disableSwiftPMIntegration, configuration.folder(workspaceFolder).searchSubfoldersForPackages, + configuration.folder(workspaceFolder).ignoreSearchingForPackagesInSubfolders, this.globalToolchainSwiftVersion ); diff --git a/src/configuration.ts b/src/configuration.ts index d01a19ee9..9d77c7a12 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -78,6 +78,8 @@ export interface FolderConfiguration { readonly additionalTestArguments: string[]; /** search sub-folder of workspace folder for Swift Packages */ readonly searchSubfoldersForPackages: boolean; + /** Folders to ignore when searching for Swift Packages */ + readonly ignoreSearchingForPackagesInSubfolders: string[]; /** auto-generate launch.json configurations */ readonly autoGenerateLaunchConfigurations: boolean; /** disable automatic running of swift package resolve */ @@ -232,6 +234,15 @@ const configuration = { .getConfiguration("swift", workspaceFolder) .get("searchSubfoldersForPackages", false); }, + /** Folders to ignore when searching for Swift Packages */ + get ignoreSearchingForPackagesInSubfolders(): string[] { + return vscode.workspace + .getConfiguration("swift", workspaceFolder) + .get< + string[] + >("ignoreSearchingForPackagesInSubfolders", [".", ".build", "Packages", "out", "bazel-out", "bazel-bin"]) + .map(substituteVariablesInString); + }, get attachmentsPath(): string { return substituteVariablesInString( vscode.workspace diff --git a/src/utilities/filesystem.ts b/src/utilities/filesystem.ts index 098177d56..064e9f4c8 100644 --- a/src/utilities/filesystem.ts +++ b/src/utilities/filesystem.ts @@ -60,6 +60,19 @@ export async function touch(path: string): Promise { } } +/** + * Checks if a folder exists at the supplied path. + * @param pathComponents The folder path to check for existence + * @returns Whether or not the folder exists at the path + */ +export async function folderExists(...pathComponents: string[]): Promise { + try { + return (await fs.stat(path.join(...pathComponents))).isDirectory(); + } catch (e) { + return false; + } +} + /** * Return whether a file/folder is inside a folder. * @param subpath child file/folder diff --git a/src/utilities/workspace.ts b/src/utilities/workspace.ts index 27ae76734..d52251615 100644 --- a/src/utilities/workspace.ts +++ b/src/utilities/workspace.ts @@ -16,30 +16,34 @@ import * as path from "path"; import { basename } from "path"; import * as vscode from "vscode"; -import { globDirectory, pathExists } from "./filesystem"; +import { folderExists, globDirectory, pathExists } from "./filesystem"; import { Version } from "./version"; export async function searchForPackages( folder: vscode.Uri, disableSwiftPMIntegration: boolean, searchSubfoldersForPackages: boolean, + skipFolders: Array, swiftVersion: Version ): Promise> { const folders: Array = []; async function search(folder: vscode.Uri) { - // add folder if Package.swift/compile_commands.json/compile_flags.txt/buildServer.json exists + // add folder if Package.swift/compile_commands.json/compile_flags.txt/buildServer.json/.bsp exists if (await isValidWorkspaceFolder(folder.fsPath, disableSwiftPMIntegration, swiftVersion)) { folders.push(folder); } - // should I search sub-folders for more Swift Packages + + // If sub-folder searches are disabled, don't search subdirectories if (!searchSubfoldersForPackages) { return; } await globDirectory(folder, { onlyDirectories: true }).then(async entries => { + const skip = new Set(skipFolders); for (const entry of entries) { - if (basename(entry) !== "." && basename(entry) !== "Packages") { + const base = basename(entry); + if (!skip.has(base)) { await search(vscode.Uri.file(entry)); } } @@ -67,7 +71,7 @@ export async function hasBSPConfigurationFile( const bspStat = await fs.stat(bspDir).catch(() => undefined); if (bspStat && bspStat.isDirectory()) { const files = await fs.readdir(bspDir).catch(() => []); - if (files.some((f: string) => f.endsWith(".json"))) { + if (files.some(f => f.endsWith(".json"))) { return true; } } @@ -94,11 +98,11 @@ export async function isValidWorkspaceFolder( return true; } - if (await pathExists(folder, "build")) { + if (await folderExists(folder, "build")) { return true; } - if (await pathExists(folder, "out")) { + if (await folderExists(folder, "out")) { return true; } diff --git a/test/integration-tests/utilities/workspace.test.ts b/test/integration-tests/utilities/workspace.test.ts index 5810dab12..5c4fef720 100644 --- a/test/integration-tests/utilities/workspace.test.ts +++ b/test/integration-tests/utilities/workspace.test.ts @@ -26,6 +26,7 @@ suite("Workspace Utilities Test Suite", () => { (vscode.workspace.workspaceFolders ?? [])[0]!.uri, false, true, + [], testSwiftVersion ); diff --git a/test/unit-tests/debugger/buildConfig.test.ts b/test/unit-tests/debugger/buildConfig.test.ts index ec6af2610..3769afc5c 100644 --- a/test/unit-tests/debugger/buildConfig.test.ts +++ b/test/unit-tests/debugger/buildConfig.test.ts @@ -38,6 +38,7 @@ suite("BuildConfig Test Suite", () => { testEnvironmentVariables: {}, additionalTestArguments, searchSubfoldersForPackages: false, + ignoreSearchingForPackagesInSubfolders: [], autoGenerateLaunchConfigurations: false, disableAutoResolve: false, attachmentsPath: "", diff --git a/test/unit-tests/utilities/workspace.test.ts b/test/unit-tests/utilities/workspace.test.ts index fdf033e1f..0b032efb7 100644 --- a/test/unit-tests/utilities/workspace.test.ts +++ b/test/unit-tests/utilities/workspace.test.ts @@ -27,13 +27,95 @@ suite("Workspace Utilities Unit Test Suite", () => { const testSwiftVersion = new Version(5, 9, 0); test("returns only root package when search for subpackages disabled", async () => { - const folders = await searchForPackages(packageFolder, false, false, testSwiftVersion); + const folders = await searchForPackages( + packageFolder, + false, + false, + [], + testSwiftVersion + ); - expect(folders.map(folder => folder.fsPath)).eql([packageFolder.fsPath]); + expect(folders.map(folder => folder.fsPath)).deep.equal([packageFolder.fsPath]); }); test("returns subpackages when search for subpackages enabled", async () => { - const folders = await searchForPackages(packageFolder, false, true, testSwiftVersion); + const folders = await searchForPackages( + packageFolder, + false, + true, + [], + testSwiftVersion + ); + + expect(folders.map(folder => folder.fsPath).sort()).deep.equal([ + packageFolder.fsPath, + firstModuleFolder.fsPath, + secondModuleFolder.fsPath, + ]); + }); + + test("skips specified folders when skipFolders contains Module1", async () => { + const folders = await searchForPackages( + packageFolder, + false, + true, + ["Module1"], + testSwiftVersion + ); + + expect(folders.map(folder => folder.fsPath).sort()).deep.equal([ + packageFolder.fsPath, + secondModuleFolder.fsPath, + ]); + }); + + test("skips specified folders when skipFolders contains Module2", async () => { + const folders = await searchForPackages( + packageFolder, + false, + true, + ["Module2"], + testSwiftVersion + ); + + expect(folders.map(folder => folder.fsPath).sort()).deep.equal([ + packageFolder.fsPath, + firstModuleFolder.fsPath, + ]); + }); + + test("skips multiple folders when skipFolders contains both modules", async () => { + const folders = await searchForPackages( + packageFolder, + false, + true, + ["Module1", "Module2"], + testSwiftVersion + ); + + expect(folders.map(folder => folder.fsPath)).deep.equal([packageFolder.fsPath]); + }); + + test("skipFolders has no effect when search for subpackages is disabled", async () => { + const folders = await searchForPackages( + packageFolder, + false, + false, + ["Module1", "Module2"], + testSwiftVersion + ); + + expect(folders.map(folder => folder.fsPath)).deep.equal([packageFolder.fsPath]); + }); + + test("skipFolders with non-existent folder names does not affect results", async () => { + const folders = await searchForPackages( + packageFolder, + false, + true, + ["NonExistentModule", "AnotherFakeModule"], + testSwiftVersion + ); expect(folders.map(folder => folder.fsPath).sort()).deep.equal([ packageFolder.fsPath,