Skip to content

Add a new Generate SourceKit-LSP Configuration command #1726

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

Merged
merged 5 commits into from
Jul 21, 2025
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Added Swiftly toolchain management support `.swift-version` files, and integration with the toolchain selection UI ([#1717](https://github.com/swiftlang/vscode-swift/pull/1717)
- Added code lenses to run suites/tests, configurable with the `swift.showTestCodeLenses` setting ([#1698](https://github.com/swiftlang/vscode-swift/pull/1698))
- New `swift.excludePathsFromActivation` setting to ignore specified sub-folders from being activated as projects ([#1693](https://github.com/swiftlang/vscode-swift/pull/1693))
- Add a `Generate SourceKit-LSP Configuration` command that creates the configuration file with versioned schema pre-populated ([#1726](https://github.com/swiftlang/vscode-swift/pull/1716))

### Fixed

Expand Down
15 changes: 15 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,11 @@
"title": "Open Documentation",
"category": "Swift",
"icon": "$(book)"
},
{
"command": "swift.generateSourcekitConfiguration",
"title": "Generate SourceKit-LSP Configuration",
"category": "Swift"
}
],
"configuration": [
Expand Down Expand Up @@ -744,6 +749,10 @@
"order": 6,
"scope": "machine-overridable"
},
"swift.sourcekit-lsp.configurationBranch": {
"type": "string",
"markdownDescription": "Set the branch to use when setting the `$schema` property of the SourceKit-LSP configuration. For example: \"release/6.1\" or \"main\". When this setting is unset, the extension will determine the branch based on the version of the toolchain that is in use."
},
"sourcekit-lsp.inlayHints.enabled": {
"type": "boolean",
"default": true,
Expand Down Expand Up @@ -1739,6 +1748,12 @@
}
]
}
],
"jsonValidation": [
{
"fileMatch": "**/.sourcekit-lsp/config.json",
"url": "https://raw.githubusercontent.com/swiftlang/sourcekit-lsp/refs/heads/main/config.schema.json"
}
]
},
"extensionDependencies": [
Expand Down
6 changes: 6 additions & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { openDocumentation } from "./commands/openDocumentation";
import restartLSPServer from "./commands/restartLSPServer";
import { generateLaunchConfigurations } from "./commands/generateLaunchConfigurations";
import { runTest } from "./commands/runTest";
import { generateSourcekitConfiguration } from "./commands/generateSourcekitConfiguration";

/**
* References:
Expand Down Expand Up @@ -105,6 +106,7 @@ export enum Commands {
OPEN_MANIFEST = "swift.openManifest",
RESTART_LSP = "swift.restartLSPServer",
SELECT_TOOLCHAIN = "swift.selectToolchain",
GENERATE_SOURCEKIT_CONFIG = "swift.generateSourcekitConfiguration",
}

/**
Expand Down Expand Up @@ -273,6 +275,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(
Commands.GENERATE_SOURCEKIT_CONFIG,
async () => await generateSourcekitConfiguration(ctx)
),
];
}

Expand Down
38 changes: 6 additions & 32 deletions src/commands/generateLaunchConfigurations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@

import { makeDebugConfigurations } from "../debugger/launch";
import { FolderContext } from "../FolderContext";
import { selectFolder } from "../ui/SelectFolderQuickPick";
import { WorkspaceContext } from "../WorkspaceContext";
import * as vscode from "vscode";

export async function generateLaunchConfigurations(ctx: WorkspaceContext): Promise<boolean> {
if (ctx.folders.length === 0) {
Expand All @@ -26,29 +26,14 @@ export async function generateLaunchConfigurations(ctx: WorkspaceContext): Promi
return await makeDebugConfigurations(ctx.folders[0], { force: true, yes: true });
}

const quickPickItems: SelectFolderQuickPick[] = ctx.folders.map(folder => ({
type: "folder",
folder,
label: folder.name,
detail: folder.workspaceFolder.uri.fsPath,
}));
quickPickItems.push({ type: "all", label: "Generate For All Folders" });
const selection = await vscode.window.showQuickPick(quickPickItems, {
matchOnDetail: true,
placeHolder: "Select a folder to generate launch configurations for",
});

if (!selection) {
const foldersToUpdate: FolderContext[] = await selectFolder(
ctx,
"Select a folder to generate launch configurations for"
);
if (!foldersToUpdate.length) {
return false;
}

const foldersToUpdate: FolderContext[] = [];
if (selection.type === "all") {
foldersToUpdate.push(...ctx.folders);
} else {
foldersToUpdate.push(selection.folder);
}

return (
await Promise.all(
foldersToUpdate.map(folder =>
Expand All @@ -57,14 +42,3 @@ export async function generateLaunchConfigurations(ctx: WorkspaceContext): Promi
)
).reduceRight((prev, curr) => prev || curr);
}

type SelectFolderQuickPick = AllQuickPickItem | FolderQuickPickItem;

interface AllQuickPickItem extends vscode.QuickPickItem {
type: "all";
}

interface FolderQuickPickItem extends vscode.QuickPickItem {
type: "folder";
folder: FolderContext;
}
121 changes: 121 additions & 0 deletions src/commands/generateSourcekitConfiguration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//===----------------------------------------------------------------------===//
//
// 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 { join } from "path";
import * as vscode from "vscode";
import { FolderContext } from "../FolderContext";
import { selectFolder } from "../ui/SelectFolderQuickPick";
import { WorkspaceContext } from "../WorkspaceContext";
import configuration from "../configuration";

export async function generateSourcekitConfiguration(ctx: WorkspaceContext): Promise<boolean> {
if (ctx.folders.length === 0) {
return false;
}

if (ctx.folders.length === 1) {
const folder = ctx.folders[0];
const success = await createSourcekitConfiguration(ctx, folder);
void vscode.window.showTextDocument(vscode.Uri.file(sourcekitConfigFilePath(folder)));
return success;
}

const foldersToGenerate: FolderContext[] = await selectFolder(
ctx,
"Select a folder to generate a SourceKit-LSP configuration for"
);
if (!foldersToGenerate.length) {
return false;
}

return (
await Promise.all(
foldersToGenerate.map(folder => createSourcekitConfiguration(ctx, folder))
)
).reduceRight((prev, curr) => prev || curr);
}

export const sourcekitFolderPath = (f: FolderContext) => join(f.folder.fsPath, ".sourcekit-lsp");
export const sourcekitConfigFilePath = (f: FolderContext) =>
join(sourcekitFolderPath(f), "config.json");

async function createSourcekitConfiguration(
workspaceContext: WorkspaceContext,
folderContext: FolderContext
): Promise<boolean> {
const sourcekitFolder = vscode.Uri.file(sourcekitFolderPath(folderContext));
const sourcekitConfigFile = vscode.Uri.file(sourcekitConfigFilePath(folderContext));

try {
await vscode.workspace.fs.stat(sourcekitConfigFile);
return true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
workspaceContext.outputChannel.appendLine(
`Failed to read file at ${sourcekitConfigFile.fsPath}: ${error}`
);
}
// Ignore, don't care if the file doesn't exist yet
}

try {
const stats = await vscode.workspace.fs.stat(sourcekitFolder);
if (stats.type !== vscode.FileType.Directory) {
void vscode.window.showErrorMessage(
`File ${sourcekitFolder.fsPath} already exists but is not a directory`
);
return false;
}
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
workspaceContext.outputChannel.appendLine(
`Failed to read folder at ${sourcekitFolder.fsPath}: ${error}`
);
}
await vscode.workspace.fs.createDirectory(sourcekitFolder);
}
const version = folderContext.toolchain.swiftVersion;
const versionString = `${version.major}.${version.minor}`;
let branch =
configuration.lsp.configurationBranch ||
(version.dev ? "main" : `release/${versionString}`);
if (!(await checkURLExists(schemaURL(branch)))) {
branch = "main";
}
await vscode.workspace.fs.writeFile(
sourcekitConfigFile,
Buffer.from(
JSON.stringify(
{
$schema: schemaURL(branch),
},
undefined,
2
)
)
);
return true;
}

const schemaURL = (branch: string) =>
`https://raw.githubusercontent.com/swiftlang/sourcekit-lsp/refs/heads/${branch}/config.schema.json`;

async function checkURLExists(url: string): Promise<boolean> {
try {
const response = await fetch(url, { method: "HEAD" });
return response.ok;
} catch {
return false;
}
}
7 changes: 7 additions & 0 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export interface LSPConfiguration {
readonly supportedLanguages: string[];
/** Is SourceKit-LSP disabled */
readonly disable: boolean;
/** Configuration branch to use when setting $schema */
readonly configurationBranch: string;
}

/** debugger configuration */
Expand Down Expand Up @@ -150,6 +152,11 @@ const configuration = {
.getConfiguration("swift.sourcekit-lsp")
.get<boolean>("disable", false);
},
get configurationBranch(): string {
return vscode.workspace
.getConfiguration("swift.sourcekit-lsp")
.get<string>("configurationBranch", "");
},
};
},

Expand Down
64 changes: 64 additions & 0 deletions src/ui/SelectFolderQuickPick.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//===----------------------------------------------------------------------===//
//
// 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 { WorkspaceContext } from "../WorkspaceContext";
import * as vscode from "vscode";
import { FolderContext } from "../FolderContext";

type SelectFolderQuickPick = AllQuickPickItem | FolderQuickPickItem;

interface AllQuickPickItem extends vscode.QuickPickItem {
type: "all";
}

interface FolderQuickPickItem extends vscode.QuickPickItem {
type: "folder";
folder: FolderContext;
}

/**
* Select a folder from the workspace context
* @param ctx
* @param labels Map "type" to the display label
* @returns The selected folder or undefined if there was no selection
*/
export async function selectFolder(
ctx: WorkspaceContext,
placeHolder: string,
labels: Record<string, string> = {}
): Promise<FolderContext[]> {
const quickPickItems: SelectFolderQuickPick[] = ctx.folders.map(folder => ({
type: "folder",
folder,
label: folder.name,
detail: folder.workspaceFolder.uri.fsPath,
}));
quickPickItems.push({ type: "all", label: labels["all"] || "Generate For All Folders" });
const selection = await vscode.window.showQuickPick(quickPickItems, {
matchOnDetail: true,
placeHolder,
});

const folders: FolderContext[] = [];
if (!selection) {
return folders;
}

if (selection.type === "all") {
folders.push(...ctx.folders);
} else {
folders.push(selection.folder);
}
return folders;
}
10 changes: 6 additions & 4 deletions src/utilities/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,21 @@ export class Version implements VersionInterface {
constructor(
readonly major: number,
readonly minor: number,
readonly patch: number
readonly patch: number,
readonly dev: boolean = false
) {}

static fromString(s: string): Version | undefined {
const numbers = s.match(/(\d+).(\d+)(?:.(\d+))?/);
const numbers = s.match(/(\d+).(\d+)(?:.(\d+))?(-dev)?/);
if (numbers) {
const major = parseInt(numbers[1]);
const minor = parseInt(numbers[2]);
const dev = numbers[4] === "-dev";
if (numbers[3] === undefined) {
return new Version(major, minor, 0);
return new Version(major, minor, 0, dev);
} else {
const patch = parseInt(numbers[3]);
return new Version(major, minor, patch);
return new Version(major, minor, patch, dev);
}
}
return undefined;
Expand Down
Loading