Skip to content

Notify user if configured $schema is out of date #1736

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 7 commits into from
Jul 23, 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
14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,16 @@
"default": "prompt",
"markdownDescription": "Controls whether to open a swift project automatically after creating it.",
"scope": "application"
},
"swift.lspConfigurationBranch": {
"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."
},
"swift.checkLspConfigurationSchema": {
"type": "boolean",
"default": true,
"markdownDescription": "When opening a .sourckit-lsp/config.json configuration file, whether or not to check if the $schema matches the version of Swift you are using.",
"scope": "machine-overridable"
}
}
},
Expand Down Expand Up @@ -749,10 +759,6 @@
"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
4 changes: 1 addition & 3 deletions src/WorkspaceContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -569,9 +569,7 @@ export class WorkspaceContext implements vscode.Disposable {
* one of those. If not then it searches up the tree to find the uppermost folder in the
* workspace that contains a Package.swift
*/
private async getPackageFolder(
url: vscode.Uri
): Promise<FolderContext | vscode.Uri | undefined> {
async getPackageFolder(url: vscode.Uri): Promise<FolderContext | vscode.Uri | undefined> {
// is editor document in any of the current FolderContexts
const folder = this.folders.find(context => {
return isPathInsidePath(url.fsPath, context.folder.fsPath);
Expand Down
137 changes: 116 additions & 21 deletions src/commands/generateSourcekitConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//
//===----------------------------------------------------------------------===//

import { join } from "path";
import { basename, dirname, join } from "path";
import * as vscode from "vscode";
import { FolderContext } from "../FolderContext";
import { selectFolder } from "../ui/SelectFolderQuickPick";
Expand Down Expand Up @@ -85,26 +85,24 @@ async function createSourcekitConfiguration(
}
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
try {
const url = await determineSchemaURL(folderContext);
await vscode.workspace.fs.writeFile(
sourcekitConfigFile,
Buffer.from(
JSON.stringify(
{
$schema: url,
},
undefined,
2
)
)
)
);
);
} catch (e) {
void vscode.window.showErrorMessage(`${e}`);
return false;
}
return true;
}

Expand All @@ -114,8 +112,105 @@ const schemaURL = (branch: string) =>
async function checkURLExists(url: string): Promise<boolean> {
try {
const response = await fetch(url, { method: "HEAD" });
return response.ok;
if (response.ok) {
return true;
} else if (response.status !== 404) {
throw new Error(`Received exit code ${response.status} when trying to fetch ${url}`);
}
return false;
} catch {
return false;
}
}

export async function determineSchemaURL(folderContext: FolderContext): Promise<string> {
const version = folderContext.toolchain.swiftVersion;
const versionString = `${version.major}.${version.minor}`;
let branch =
configuration.lspConfigurationBranch || (version.dev ? "main" : `release/${versionString}`);
if (!(await checkURLExists(schemaURL(branch)))) {
branch = "main";
}
return schemaURL(branch);
}

async function checkDocumentSchema(doc: vscode.TextDocument, workspaceContext: WorkspaceContext) {
const folder = await workspaceContext.getPackageFolder(doc.uri);
if (!folder) {
return;
}
const folderContext = folder as FolderContext;
if (!folderContext.name) {
return; // Not a FolderContext if no "name"
}
let buffer: Uint8Array;
try {
buffer = await vscode.workspace.fs.readFile(doc.uri);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
workspaceContext.outputChannel.appendLine(
`Failed to read file at ${doc.uri.fsPath}: ${error}`
);
}
return;
}
let config;
try {
const contents = Buffer.from(buffer).toString("utf-8");
config = JSON.parse(contents);
} catch (error) {
workspaceContext.outputChannel.appendLine(
`Failed to parse JSON from ${doc.uri.fsPath}: ${error}`
);
return;
}
const schema = config.$schema;
if (!schema) {
return;
}
const newUrl = await determineSchemaURL(folderContext);
if (newUrl === schema) {
return;
}
const result = await vscode.window.showInformationMessage(
`The $schema property for ${doc.uri.fsPath} is not set to the version of the Swift toolchain that you are using. Would you like to update the $schema property?`,
"Yes",
"No",
"Don't Ask Again"
);
if (result === "Yes") {
config.$schema = newUrl;
await vscode.workspace.fs.writeFile(
doc.uri,
Buffer.from(JSON.stringify(config, undefined, 2))
);
return;
} else if (result === "Don't Ask Again") {
configuration.checkLspConfigurationSchema = false;
return;
}
}

export async function handleSchemaUpdate(
doc: vscode.TextDocument,
workspaceContext: WorkspaceContext
) {
if (
!configuration.checkLspConfigurationSchema ||
!(
basename(dirname(doc.uri.fsPath)) === ".sourcekit-lsp" &&
basename(doc.uri.fsPath) === "config.json"
)
) {
return;
}
await checkDocumentSchema(doc, workspaceContext);
}

export function registerSourceKitSchemaWatcher(
workspaceContext: WorkspaceContext
): vscode.Disposable {
return vscode.workspace.onDidOpenTextDocument(doc => {
void handleSchemaUpdate(doc, workspaceContext);
});
}
23 changes: 16 additions & 7 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ 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 @@ -152,11 +150,6 @@ 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 Expand Up @@ -503,6 +496,22 @@ const configuration = {
.getConfiguration("swift")
.get<Record<string, boolean>>("excludePathsFromActivation", {});
},
get lspConfigurationBranch(): string {
return vscode.workspace.getConfiguration("swift").get<string>("lspConfigurationBranch", "");
},
get checkLspConfigurationSchema(): boolean {
return vscode.workspace
.getConfiguration("swift")
.get<boolean>("checkLspConfigurationSchema", true);
},
set checkLspConfigurationSchema(value: boolean) {
void vscode.workspace
.getConfiguration("swift")
.update("checkLspConfigurationSchema", value)
.then(() => {
/* Put in worker queue */
});
},
};

const vsCodeVariableRegex = new RegExp(/\$\{(.+?)\}/g);
Expand Down
3 changes: 3 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { resolveFolderDependencies } from "./commands/dependencies/resolve";
import { SelectedXcodeWatcher } from "./toolchain/SelectedXcodeWatcher";
import configuration, { handleConfigurationChangeEvent } from "./configuration";
import contextKeys from "./contextKeys";
import { registerSourceKitSchemaWatcher } from "./commands/generateSourcekitConfiguration";

/**
* External API as exposed by the extension. Can be queried by other extensions
Expand Down Expand Up @@ -136,6 +137,8 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api> {
);
context.subscriptions.push(TestExplorer.observeFolders(workspaceContext));

context.subscriptions.push(registerSourceKitSchemaWatcher(workspaceContext));

// setup workspace context with initial workspace folders
void workspaceContext.addWorkspaceFolders();

Expand Down
Loading