Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,11 @@
"command": "swift.generateSourcekitConfiguration",
"title": "Generate SourceKit-LSP Configuration",
"category": "Swift"
},
{
"command": "swift.createDocumentationCatalog",
"title": "Create Documentation Catalog",
"category": "Swift"
}
],
"configuration": [
Expand Down Expand Up @@ -1382,6 +1387,10 @@
{
"command": "swift.play",
"when": "false"
},
{
"command": "swift.createDocumentationCatalog",
"when": "workspaceFolderCount > 0"
}
],
"editor/context": [
Expand Down
5 changes: 5 additions & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { WorkspaceContext } from "./WorkspaceContext";
import { attachDebugger } from "./commands/attachDebugger";
import { cleanBuild, debugBuild, runBuild } from "./commands/build";
import { captureDiagnostics } from "./commands/captureDiagnostics";
import { createDocumentationCatalog } from "./commands/createDocumentationCatalog";
import { createNewProject } from "./commands/createNewProject";
import { editDependency } from "./commands/dependencies/edit";
import { resolveDependencies } from "./commands/dependencies/resolve";
Expand Down Expand Up @@ -350,6 +351,10 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] {
await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(packagePath));
}),
vscode.commands.registerCommand("swift.openDocumentation", () => openDocumentation()),
vscode.commands.registerCommand(
"swift.createDocumentationCatalog",
async () => await createDocumentationCatalog()
),
vscode.commands.registerCommand(
Commands.GENERATE_SOURCEKIT_CONFIG,
async () => await generateSourcekitConfiguration(ctx)
Expand Down
150 changes: 150 additions & 0 deletions src/commands/createDocumentationCatalog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the VS Code Swift open source project
//
// Copyright (c) 2025 the VS Code Swift project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import { execFile } from "child_process";
import * as fs from "fs/promises";
import * as path from "path";
import { promisify } from "util";
import * as vscode from "vscode";

const execFileAsync = promisify(execFile);

type DoccLocationPickItem = vscode.QuickPickItem & {
basePath: string;
};

export async function createDocumentationCatalog(): Promise<void> {
const folders = vscode.workspace.workspaceFolders;
if (!folders || folders.length === 0) {
void vscode.window.showErrorMessage(
"Creating a documentation catalog requires that a folder or workspace be opened."
);
return;
}

let folder: vscode.WorkspaceFolder | undefined;

if (folders.length === 1) {
folder = folders[0];
} else {
folder = await vscode.window.showWorkspaceFolderPick({
placeHolder: "Select a workspace folder to create the DocC catalog in",
});
}

if (!folder) {
return;
}

const rootPath = folder.uri.fsPath;

let hasPackageSwift = true;
try {
await fs.access(path.join(rootPath, "Package.swift"));
} catch {
hasPackageSwift = false;
}

let targets: string[] = [];

if (hasPackageSwift) {
try {
const { stdout } = await execFileAsync("swift", ["package", "dump-package"], {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This information is already computed and stored on the folderContext.swiftPackage for each Swift Package folder open in the workspace. See https://github.com/swiftlang/vscode-swift/blob/main/src/commands.ts#L80 for a reference of how to get the folderContext for the currently active folder and pass it in to the command.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out — I wasn’t aware this information was already available via FolderContext. I’ll refactor the command to use folderContext.swiftPackage instead of invoking swift package dump-package.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

cwd: rootPath,
});

const pkg = JSON.parse(stdout);
targets = pkg.targets.map((t: { name: string }) => t.name);
} catch {
// If SwiftPM fails, fall back to standalone
targets = [];
}
}

const items: DoccLocationPickItem[] = [];

for (const name of targets) {
const srcPath = path.join(rootPath, "Sources", name);
try {
await fs.access(srcPath);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use fileExists in utilities/filesystem instead

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

once i read filesystem i think folderExists is much more accurate in this case

Copy link
Member

@matthewbastien matthewbastien Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fileExists() and folderExists() check for the path in question to be a file or folder respectively. You're going to want to use pathExists() because you can't create the folder if anything exists at that path regardless of whether or not it is a file, folder, symlink, etc.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matthewbastien Target discovery still uses folderExists() since those must be directories.

items.push({
label: `Target: ${name}`,
description: `Sources/${name}`,
basePath: srcPath,
});
} catch {
//skip
}

const testPath = path.join(rootPath, "Tests", name);
try {
await fs.access(testPath);
items.push({
label: `Target: ${name}`,
description: `Tests/${name}`,
basePath: testPath,
});
} catch {
//skip
}
}

items.push({
label: "Standalone documentation catalog",
description: "Workspace root",
basePath: rootPath,
});

const selection = await vscode.window.showQuickPick<DoccLocationPickItem>(items, {
placeHolder: "Select where to create the documentation catalog",
});

if (!selection) {
return;
}

const basePath = selection.basePath;

const moduleName = await vscode.window.showInputBox({
prompt: "Enter Swift module name",
placeHolder: "MyModule",
validateInput: async value => {
if (value.trim().length === 0) {
return "Module name cannot be empty";
}

const doccDir = path.join(basePath, `${value}.docc`);
try {
await fs.access(doccDir);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fileExists here as well

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think folderExists is much accurate here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll have to use pathExists().

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, i have made that commit now

return `Documentation catalog "${value}.docc" already exists`;
} catch {
// does not exist → OK
return undefined;
}
},
});

if (!moduleName) {
return; // user cancelled
}

const doccDir = path.join(basePath, `${moduleName}.docc`);
const markdownFile = path.join(doccDir, `${moduleName}.md`);

await fs.mkdir(doccDir, { recursive: true });
await fs.writeFile(markdownFile, `# ${moduleName}\n`, "utf8");

void vscode.window.showInformationMessage(
`Created DocC documentation catalog: ${moduleName}.docc`
);
}