Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
43e2518
Add command to create Swift DocC documentation catalog
amanthatdoescares Dec 17, 2025
81461d5
Add command to create Swift DocC documentation catalog
amanthatdoescares Dec 17, 2025
6291552
restoring package-lock-json
amanthatdoescares Dec 17, 2025
3277a02
Use async filesystem APIs, Included Copyright Headers, changed the va…
amanthatdoescares Dec 17, 2025
a528243
Validate Docc Catalog exisitance inside input
amanthatdoescares Dec 17, 2025
62a4225
Ask user to select the workspace folder and validate docc name inline
amanthatdoescares Dec 18, 2025
907fdfb
Update src/commands/createDocumentationCatalog.ts
amanthatdoescares Dec 18, 2025
1a6fb2f
Update src/commands/createDocumentationCatalog.ts
amanthatdoescares Dec 18, 2025
7343dae
Use async filesystem check
amanthatdoescares Dec 18, 2025
dd3533b
Add target-based QuickPick for DocC catalog creation
amanthatdoescares Dec 18, 2025
3a1d262
Use folderExists for directory check
amanthatdoescares Dec 19, 2025
3ec91e0
Using folderContext.swiftpackage
amanthatdoescares Dec 19, 2025
d74fb7c
Use pathExists for DocC creation checks
amanthatdoescares Dec 20, 2025
3b8853c
Add integration test for createDocumentationCatalog command
amanthatdoescares Dec 20, 2025
2410af8
Update src/commands/createDocumentationCatalog.ts
amanthatdoescares Dec 22, 2025
f991a9c
remove nested test, mark suite as small, added right header
amanthatdoescares Dec 22, 2025
225a1d4
Use targetName as Docc name
amanthatdoescares Dec 22, 2025
184b533
Fix test to use selected target name for DocC catalog
amanthatdoescares Dec 23, 2025
2bec57e
added test for standalone docC catalog
amanthatdoescares Dec 23, 2025
0baba1a
commit h1
amanthatdoescares Dec 23, 2025
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) {
Copy link
Member

Choose a reason for hiding this comment

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

question: Do we really need to ask which folder to use before showing available targets? We can just remove a step here and show all of the available targets from all known folders. We can also use separators if needed in the quick pick to show which folder each target came from.

If the user selects a standalone documentation catalog and there are multiple folders then we can ask.

Copy link
Author

Choose a reason for hiding this comment

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

That makes sense and I agree the flow would be cleaner. I’ll keep the current behavior for now to finish addressing the existing feedback, and then follow up with the multi-folder target selection improvement.

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({
Copy link
Member

Choose a reason for hiding this comment

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

issue: By convention the module name for a target should match the target name if the user selected one in the previous step. The extension should only ask for a folder if the user selected a standalone documentation catalog.

Copy link
Author

Choose a reason for hiding this comment

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

done

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 () {
Copy link
Member

Choose a reason for hiding this comment

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

issue: This suite should be marked as either "medium" or "small". It definitely doesn't need 10 minutes just to create a directory. I'm leaning towards "small" in this instance since it really shouldn't take long at all.

Copy link
Author

Choose a reason for hiding this comment

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

i changed it to small

let folderContext: FolderContext;
let workspaceContext: WorkspaceContext;

activateExtensionForSuite({
async setup(ctx) {
workspaceContext = ctx;
folderContext = await folderInRootWorkspace("defaultPackage", workspaceContext);
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: You can add multiple asset folders here to properly test the logic of finding targets within multiple Package.swift.

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");
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: We have methods in MockUtils.ts that will do this for you using mocha's setup() and teardown() blocks. You don't need to use sinon.stub() or the try {} finally {} here. For example:

import { mockGlobalObject } from "../../MockUtils.ts";

suite("some-suite", () => {
    const windowMock = mockGlobalObject(vscode, "window");

    test("some-test", () => {
        windowMock.showQuickPick.resolves("blah");
    });
});

const inputBoxStub = sinon.stub(vscode.window, "showInputBox");

try {
inputBoxStub.resolves("MyModule");
Copy link
Member

Choose a reason for hiding this comment

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

issue: The module name quick pick should not be shown if selecting a target.

quickPickStub.callsFake(async itemsOrPromise => {
const items = await Promise.resolve(itemsOrPromise);
return items.find(item => item.label.startsWith("Target:"));
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: Validate whether or not the list of targets contains all of the targets that we expect to be there.

});

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");
Copy link
Member

Choose a reason for hiding this comment

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

issue: The module name when selecting a target should match the module name of that target.

Copy link
Author

Choose a reason for hiding this comment

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

yes, done

} finally {
quickPickStub.restore();
inputBoxStub.restore();
}
});
});
});
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: We should also test the logic for creating a standalone documentation catalog.

Copy link
Author

Choose a reason for hiding this comment

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

added