-
Notifications
You must be signed in to change notification settings - Fork 90
Add command to create a Swift DocC documentation catalog #2006
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
43e2518
81461d5
6291552
3277a02
a528243
62a4225
907fdfb
1a6fb2f
7343dae
dd3533b
3a1d262
3ec91e0
d74fb7c
3b8853c
2410af8
f991a9c
225a1d4
184b533
2bec57e
0baba1a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| // | ||
| // 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; | ||
| targetName?: 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) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = (async () => { | ||
| const items: DoccLocationPickItem[] = []; | ||
|
|
||
| if (folder.swiftPackage) { | ||
| const targets = await folder.swiftPackage.getTargets(); | ||
|
|
||
| for (const target of targets) { | ||
| const base = path.join(rootPath, target.path); | ||
| if (await folderExists(base)) { | ||
| items.push({ | ||
| label: `Target: ${target.name}`, | ||
| description: target.type, | ||
| detail: target.path, | ||
| basePath: base, | ||
| targetName: target.name, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 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 ---- | ||
| let moduleName: string; | ||
|
|
||
| if (selection.targetName) { | ||
| // Target-based DocC: module name must match target | ||
| moduleName = selection.targetName; | ||
| } else { | ||
| // Standalone DocC: ask user | ||
| const input = 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`); | ||
| if (await pathExists(doccDir)) { | ||
| return `Documentation catalog "${name}.docc" already exists`; | ||
| } | ||
|
|
||
| return undefined; | ||
| }, | ||
| }); | ||
|
|
||
| if (!input) { | ||
| return; | ||
| } | ||
|
|
||
| moduleName = input.trim(); | ||
| } | ||
|
|
||
| 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,93 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // 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 | ||
amanthatdoescares marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // | ||
| // 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 { expect } from "chai"; | ||
| import * as fs from "fs/promises"; | ||
| import * as path from "path"; | ||
| import * as vscode from "vscode"; | ||
|
|
||
| import { FolderContext } from "@src/FolderContext"; | ||
| import { WorkspaceContext } from "@src/WorkspaceContext"; | ||
|
|
||
| import { mockGlobalObject } from "../../MockUtils"; | ||
| import { tag } from "../../tags"; | ||
| import { activateExtensionForSuite, folderInRootWorkspace } from "../utilities/testutilities"; | ||
|
|
||
| tag("small").suite("Create Documentation Catalog Command", function () { | ||
| let folderContext: FolderContext; | ||
| let workspaceContext: WorkspaceContext; | ||
| let windowMock: ReturnType<typeof mockGlobalObject>; | ||
|
|
||
| activateExtensionForSuite({ | ||
| async setup(ctx) { | ||
| workspaceContext = ctx; | ||
| folderContext = await folderInRootWorkspace("defaultPackage", workspaceContext); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| await workspaceContext.focusFolder(folderContext); | ||
| }, | ||
| }); | ||
|
|
||
| setup(() => { | ||
| windowMock = mockGlobalObject(vscode, "window"); | ||
| }); | ||
|
|
||
| teardown(() => { | ||
| windowMock.restore(); | ||
| }); | ||
|
|
||
| test("creates a DocC catalog for a SwiftPM target", async () => { | ||
| let selectedTargetLabel: string | undefined; | ||
|
|
||
| windowMock.showQuickPick.callsFake( | ||
| async (itemsOrPromise: vscode.QuickPickItem[] | Thenable<vscode.QuickPickItem[]>) => { | ||
| const items = await Promise.resolve(itemsOrPromise); | ||
| const target = items.find(item => item.label.startsWith("Target:")); | ||
| selectedTargetLabel = target?.label; | ||
| return target; | ||
| } | ||
| ); | ||
|
|
||
| // This path must not prompt for a module name | ||
| windowMock.showInputBox.rejects(new Error("showInputBox should not be called")); | ||
|
|
||
| await vscode.commands.executeCommand("swift.createDocumentationCatalog"); | ||
|
|
||
| const basePath = folderContext.folder.fsPath; | ||
| const moduleName = selectedTargetLabel!.replace("Target: ", ""); | ||
| const doccDir = path.join(basePath, `${moduleName}.docc`); | ||
| const markdownFile = path.join(doccDir, `${moduleName}.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(`# ${moduleName}`); | ||
| }); | ||
|
|
||
| test("creates a standalone DocC catalog when no SwiftPM target is selected", async () => { | ||
| windowMock.showQuickPick.resolves(undefined); | ||
| windowMock.showInputBox.resolves("StandaloneModule"); | ||
|
|
||
| await vscode.commands.executeCommand("swift.createDocumentationCatalog"); | ||
|
|
||
| const basePath = folderContext.folder.fsPath; | ||
| const moduleName = "StandaloneModule"; | ||
| const doccDir = path.join(basePath, `${moduleName}.docc`); | ||
| const markdownFile = path.join(doccDir, `${moduleName}.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(`# ${moduleName}`); | ||
| }); | ||
| }); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added |
||
Uh oh!
There was an error while loading. Please reload this page.