Skip to content

Commit 4e46f45

Browse files
committed
Add dynamic local schema detection for SourceKit-LSP
Implement support for using local schema files from the Swift toolchain when available, with automatic fallback to remote GitHub schemas for older toolchains. This enables the extension to use the accurate schema for the users toolchain, if its present, while maintaining backward compatibility with older toolchains.
1 parent 8b1bcf9 commit 4e46f45

File tree

4 files changed

+341
-2
lines changed

4 files changed

+341
-2
lines changed

src/WorkspaceContext.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { CommentCompletionProviders } from "./editor/CommentCompletion";
2828
import { SwiftLogger } from "./logging/SwiftLogger";
2929
import { SwiftLoggerFactory } from "./logging/SwiftLoggerFactory";
3030
import { LanguageClientToolchainCoordinator } from "./sourcekit-lsp/LanguageClientToolchainCoordinator";
31+
import { SourcekitSchemaRegistry } from "./sourcekit-lsp/SourcekitSchemaRegistry";
3132
import { DocCDocumentationRequest, ReIndexProjectRequest } from "./sourcekit-lsp/extensions";
3233
import { SwiftPluginTaskProvider } from "./tasks/SwiftPluginTaskProvider";
3334
import { SwiftTaskProvider } from "./tasks/SwiftTaskProvider";
@@ -62,6 +63,7 @@ export class WorkspaceContext implements vscode.Disposable {
6263
public documentation: DocumentationManager;
6364
public testRunManager: TestRunManager;
6465
public projectPanel: ProjectPanelProvider;
66+
private sourcekitSchemaRegistry: SourcekitSchemaRegistry;
6567
private lastFocusUri: vscode.Uri | undefined;
6668
private initialisationFinished = false;
6769

@@ -105,6 +107,7 @@ export class WorkspaceContext implements vscode.Disposable {
105107
this.currentDocument = null;
106108
this.commentCompletionProvider = new CommentCompletionProviders();
107109
this.projectPanel = new ProjectPanelProvider(this);
110+
this.sourcekitSchemaRegistry = new SourcekitSchemaRegistry(this);
108111

109112
const onChangeConfig = vscode.workspace.onDidChangeConfiguration(async event => {
110113
// Clear build path cache when build-related configurations change
@@ -229,6 +232,7 @@ export class WorkspaceContext implements vscode.Disposable {
229232
this.statusItem,
230233
this.buildStatus,
231234
this.projectPanel,
235+
this.sourcekitSchemaRegistry.register(),
232236
];
233237
this.lastFocusUri = vscode.window.activeTextEditor?.document.uri;
234238

@@ -244,6 +248,7 @@ export class WorkspaceContext implements vscode.Disposable {
244248
}
245249

246250
dispose() {
251+
this.sourcekitSchemaRegistry.dispose();
247252
this.folders.forEach(f => f.dispose());
248253
this.folders.length = 0;
249254
this.subscriptions.forEach(item => item.dispose());

src/commands/generateSourcekitConfiguration.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ import { FolderContext } from "../FolderContext";
1818
import { WorkspaceContext } from "../WorkspaceContext";
1919
import configuration from "../configuration";
2020
import { selectFolder } from "../ui/SelectFolderQuickPick";
21+
import { fileExists } from "../utilities/filesystem";
2122
import restartLSPServer from "./restartLSPServer";
2223

23-
export const sourcekitDotFolder: string = ".sourcekit-lsp";
24-
export const sourcekitConfigFileName: string = "config.json";
24+
const sourcekitDotFolder: string = ".sourcekit-lsp";
25+
const sourcekitConfigFileName: string = "config.json";
2526

2627
export async function generateSourcekitConfiguration(ctx: WorkspaceContext): Promise<boolean> {
2728
if (ctx.folders.length === 0) {
@@ -128,6 +129,13 @@ async function checkURLExists(url: string): Promise<boolean> {
128129
}
129130

130131
export async function determineSchemaURL(folderContext: FolderContext): Promise<string> {
132+
// Check if local schema exists first
133+
if (await hasLocalSchema(folderContext)) {
134+
const localPath = localSchemaPath(folderContext);
135+
return vscode.Uri.file(localPath).toString();
136+
}
137+
138+
// Fall back to remote URL for older toolchains
131139
const version = folderContext.toolchain.swiftVersion;
132140
const versionString = `${version.major}.${version.minor}`;
133141
let branch =
@@ -138,6 +146,26 @@ export async function determineSchemaURL(folderContext: FolderContext): Promise<
138146
return schemaURL(branch);
139147
}
140148

149+
/**
150+
* Returns the path to the local sourcekit-lsp schema file in the toolchain.
151+
* This file only exists in newer toolchains (approximately Swift 6.3+).
152+
*/
153+
export function localSchemaPath(folderContext: FolderContext): string {
154+
return join(
155+
folderContext.toolchain.toolchainPath,
156+
"share",
157+
"sourcekit-lsp",
158+
"config.schema.json"
159+
);
160+
}
161+
162+
/**
163+
* Checks if the local schema file exists in the toolchain.
164+
*/
165+
export async function hasLocalSchema(folderContext: FolderContext): Promise<boolean> {
166+
return await fileExists(localSchemaPath(folderContext));
167+
}
168+
141169
async function getValidatedFolderContext(
142170
uri: vscode.Uri,
143171
workspaceContext: WorkspaceContext
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2025 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
import * as vscode from "vscode";
15+
16+
import { FolderContext } from "../FolderContext";
17+
import { WorkspaceContext } from "../WorkspaceContext";
18+
import {
19+
determineSchemaURL,
20+
sourcekitConfigFilePath,
21+
} from "../commands/generateSourcekitConfiguration";
22+
23+
/**
24+
* Manages dynamic JSON schema associations for sourcekit-lsp config files.
25+
* This allows VS Code to provide validation and IntelliSense using the
26+
* appropriate schema (local or remote) based on the toolchain.
27+
*/
28+
export class SourcekitSchemaRegistry {
29+
private disposables: vscode.Disposable[] = [];
30+
31+
constructor(private workspaceContext: WorkspaceContext) {}
32+
33+
/**
34+
* Registers event handlers to dynamically configure JSON schemas
35+
* for sourcekit-lsp config documents.
36+
*/
37+
register(): vscode.Disposable {
38+
// Handle documents that are already open
39+
vscode.workspace.textDocuments.forEach(doc => {
40+
void this.configureSchemaForDocument(doc);
41+
});
42+
43+
// Handle newly opened documents
44+
const onDidOpenDisposable = vscode.workspace.onDidOpenTextDocument(doc => {
45+
void this.configureSchemaForDocument(doc);
46+
});
47+
48+
this.disposables.push(onDidOpenDisposable);
49+
50+
return vscode.Disposable.from(...this.disposables);
51+
}
52+
53+
/**
54+
* Configures the JSON schema for a document if it's a sourcekit-lsp config file.
55+
*/
56+
private async configureSchemaForDocument(document: vscode.TextDocument): Promise<void> {
57+
if (document.languageId !== "json") {
58+
return;
59+
}
60+
61+
const folderContext = await this.getFolderContextForDocument(document);
62+
if (!folderContext) {
63+
return;
64+
}
65+
66+
const schemaUrl = await determineSchemaURL(folderContext);
67+
68+
// Use VS Code's JSON language configuration API to associate the schema
69+
await vscode.commands.executeCommand("json.setSchema", document.uri.toString(), schemaUrl);
70+
}
71+
72+
/**
73+
* Gets the FolderContext for a document if it's a sourcekit-lsp config file.
74+
*/
75+
private async getFolderContextForDocument(
76+
document: vscode.TextDocument
77+
): Promise<FolderContext | null> {
78+
for (const folderContext of this.workspaceContext.folders) {
79+
const configPath = sourcekitConfigFilePath(folderContext);
80+
if (document.uri.fsPath === configPath) {
81+
return folderContext;
82+
}
83+
}
84+
return null;
85+
}
86+
87+
dispose() {
88+
this.disposables.forEach(d => d.dispose());
89+
this.disposables = [];
90+
}
91+
}

0 commit comments

Comments
 (0)