From b2a9af75785a4b62bba929eede4afb07e2363c49 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Mon, 29 Sep 2025 11:21:53 -0400 Subject: [PATCH 1/4] Ensure the extension activates with a .bsp folder A .bsp folder was considered a valid workspace folder, but the extension acitvation events in the package.json were not configured to search for it, meaning another valid file/folder for activation had to be present. If that was the case, then the .bsp folder would be discovered correctly, but not if it was the only file/folder in the folder that would activate the extension. Add it to the list of valid activation file types. Also clean up this code path a bit, ignoring common folders we shouldn't search for projects. --- package.json | 1 + src/utilities/filesystem.ts | 13 +++++++++++++ src/utilities/workspace.ts | 22 ++++++++++++++++------ 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 21af60a52..f2b3ed4c8 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" ], 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..182dc923d 100644 --- a/src/utilities/workspace.ts +++ b/src/utilities/workspace.ts @@ -16,7 +16,7 @@ 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( @@ -28,7 +28,7 @@ export async function searchForPackages( 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); } @@ -38,8 +38,18 @@ export async function searchForPackages( } await globDirectory(folder, { onlyDirectories: true }).then(async entries => { + const skipFolders = new Set([ + ".", + ".build", + "Packages", + "out", + "bazel-out", + "bazel-bin", + ]); + for (const entry of entries) { - if (basename(entry) !== "." && basename(entry) !== "Packages") { + const base = basename(entry); + if (!skipFolders.has(base)) { await search(vscode.Uri.file(entry)); } } @@ -67,7 +77,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 +104,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; } From 668bdc1ef06fb24a3e60c42f5f5d10fc5e387ded Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Mon, 6 Oct 2025 11:10:29 -0400 Subject: [PATCH 2/4] Add ignoreSearchingForPackagesInSubfolders setting --- package.json | 16 ++++ src/WorkspaceContext.ts | 1 + src/configuration.ts | 11 +++ src/utilities/workspace.ts | 16 ++-- .../utilities/workspace.test.ts | 1 + test/unit-tests/utilities/workspace.test.ts | 88 ++++++++++++++++++- 6 files changed, 119 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index f2b3ed4c8..8abfae5db 100644 --- a/package.json +++ b/package.json @@ -545,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/workspace.ts b/src/utilities/workspace.ts index 182dc923d..d52251615 100644 --- a/src/utilities/workspace.ts +++ b/src/utilities/workspace.ts @@ -23,6 +23,7 @@ export async function searchForPackages( folder: vscode.Uri, disableSwiftPMIntegration: boolean, searchSubfoldersForPackages: boolean, + skipFolders: Array, swiftVersion: Version ): Promise> { const folders: Array = []; @@ -32,24 +33,17 @@ export async function searchForPackages( 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 skipFolders = new Set([ - ".", - ".build", - "Packages", - "out", - "bazel-out", - "bazel-bin", - ]); - + const skip = new Set(skipFolders); for (const entry of entries) { const base = basename(entry); - if (!skipFolders.has(base)) { + if (!skip.has(base)) { await search(vscode.Uri.file(entry)); } } 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/utilities/workspace.test.ts b/test/unit-tests/utilities/workspace.test.ts index fdf033e1f..b0af953ed 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)).equals([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)).equals([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)).equals([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, From e8f15feb031ddd2cae7ffc37f5957fc5a8972cef Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Mon, 6 Oct 2025 12:57:01 -0400 Subject: [PATCH 3/4] Rebase and fixup type declaration --- test/unit-tests/debugger/buildConfig.test.ts | 1 + 1 file changed, 1 insertion(+) 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: "", From fd2f98158c62a17d40056d9ddb3370268b774f82 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Mon, 6 Oct 2025 13:19:20 -0400 Subject: [PATCH 4/4] Fixup equality check --- test/unit-tests/utilities/workspace.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit-tests/utilities/workspace.test.ts b/test/unit-tests/utilities/workspace.test.ts index b0af953ed..0b032efb7 100644 --- a/test/unit-tests/utilities/workspace.test.ts +++ b/test/unit-tests/utilities/workspace.test.ts @@ -35,7 +35,7 @@ suite("Workspace Utilities Unit Test Suite", () => { testSwiftVersion ); - expect(folders.map(folder => folder.fsPath)).equals([packageFolder.fsPath]); + expect(folders.map(folder => folder.fsPath)).deep.equal([packageFolder.fsPath]); }); test("returns subpackages when search for subpackages enabled", async () => { @@ -93,7 +93,7 @@ suite("Workspace Utilities Unit Test Suite", () => { testSwiftVersion ); - expect(folders.map(folder => folder.fsPath)).equals([packageFolder.fsPath]); + expect(folders.map(folder => folder.fsPath)).deep.equal([packageFolder.fsPath]); }); test("skipFolders has no effect when search for subpackages is disabled", async () => { @@ -105,7 +105,7 @@ suite("Workspace Utilities Unit Test Suite", () => { testSwiftVersion ); - expect(folders.map(folder => folder.fsPath)).equals([packageFolder.fsPath]); + expect(folders.map(folder => folder.fsPath)).deep.equal([packageFolder.fsPath]); }); test("skipFolders with non-existent folder names does not affect results", async () => {