Skip to content
Open
Show file tree
Hide file tree
Changes from all 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(ctx)
),
vscode.commands.registerCommand(
Commands.GENERATE_SOURCEKIT_CONFIG,
async () => await generateSourcekitConfiguration(ctx)
Expand Down
138 changes: 138 additions & 0 deletions src/commands/createDocumentationCatalog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//===----------------------------------------------------------------------===//
//
// 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
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import * as fs from "fs/promises";
import * as path from "path";
import * as vscode from "vscode";

import { FolderContext } from "../FolderContext";
import { WorkspaceContext } from "../WorkspaceContext";
import { selectFolder } from "../ui/SelectFolderQuickPick";
import { folderExists, pathExists } from "../utilities/filesystem";

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

export async function createDocumentationCatalog(
ctx: WorkspaceContext,
folderContext?: FolderContext
): Promise<void> {
let folder = folderContext ?? ctx.currentFolder;

// ---- workspace folder resolution (standard pattern) ----
if (!folder) {
if (ctx.folders.length === 0) {
void vscode.window.showErrorMessage(
"Creating a documentation catalog requires an open workspace folder."
);
return;
}

if (ctx.folders.length === 1) {
folder = ctx.folders[0];
} else {
const selected = await selectFolder(
ctx,
"Select a workspace folder to create the DocC catalog in",
{ all: "" }
);
if (selected.length !== 1) {
return;
}
folder = selected[0];
}
}

const rootPath = folder.folder.fsPath;

// ---- build QuickPick items from swiftPackage (PROMISE) ----
const itemsPromise = folder.swiftPackage.getTargets().then(async targets => {
const items: DoccLocationPickItem[] = [];

for (const target of targets) {
const base = path.join(rootPath, target.path);

// target paths must be directories → folderExists is correct here
if (await folderExists(base)) {
items.push({
label: `Target: ${target.name}`,
description: target.type,
detail: target.path,
basePath: base,
});
}
}

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

return items;
});

// ---- show QuickPick (toolchain-style pattern) ----
const selection = await vscode.window.showQuickPick(itemsPromise, {
title: "Create DocC Documentation Catalog",
placeHolder: "Select where to create the documentation catalog",
canPickMany: false,
});

if (!selection) {
return;
}

const basePath = selection.basePath;

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

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

// creation path → must be unused → pathExists
if (await pathExists(doccDir)) {
return `Documentation catalog "${name}.docc" already exists`;
}

return undefined;
},
});

if (!moduleName) {
return;
}

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

// ---- execution-time guard (race-safe) ----
if (await pathExists(doccDir)) {
void vscode.window.showErrorMessage(
`Documentation catalog "${moduleName}.docc" already exists`
);
return;
}

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

void vscode.window.showInformationMessage(
`Created DocC documentation catalog: ${moduleName}.docc`
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//===----------------------------------------------------------------------===//
//
// 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
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import { expect } from "chai";
import * as fs from "fs/promises";
import * as path from "path";
import * as sinon from "sinon";
import * as vscode from "vscode";

import { FolderContext } from "@src/FolderContext";
import { WorkspaceContext } from "@src/WorkspaceContext";

import { tag } from "../../tags";
import { activateExtensionForSuite, folderInRootWorkspace } from "../utilities/testutilities";

tag("large").suite("Create Documentation Catalog Command", function () {
let folderContext: FolderContext;
let workspaceContext: WorkspaceContext;

activateExtensionForSuite({
async setup(ctx) {
workspaceContext = ctx;
folderContext = await folderInRootWorkspace("defaultPackage", workspaceContext);
await workspaceContext.focusFolder(folderContext);
},
});

test("creates a DocC catalog for a SwiftPM target", async () => {
test("creates a DocC catalog for a SwiftPM target", async () => {
const quickPickStub = sinon.stub(vscode.window, "showQuickPick");
const inputBoxStub = sinon.stub(vscode.window, "showInputBox");

try {
inputBoxStub.resolves("MyModule");
quickPickStub.callsFake(async itemsOrPromise => {
const items = await Promise.resolve(itemsOrPromise);
return items.find(item => item.label.startsWith("Target:"));
});

await vscode.commands.executeCommand("swift.createDocumentationCatalog");
const basePath = folderContext.folder.fsPath;
const doccDir = path.join(basePath, "MyModule.docc");
const markdownFile = path.join(doccDir, "MyModule.md");

expect(await fs.stat(doccDir)).to.exist;
expect(await fs.stat(markdownFile)).to.exist;

const contents = await fs.readFile(markdownFile, "utf8");
expect(contents).to.contain("# MyModule");
} finally {
quickPickStub.restore();
inputBoxStub.restore();
}
});
});
});