Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## {{releaseVersion}} - {{releaseDate}}

### Added

- When editing .sourcekit-lsp/config.json use the JSON schema from the toolchain ([#1979](https://github.com/swiftlang/vscode-swift/pull/1979))

### Fixed

- Fix extension failing to activate when Swiftly was installed via Homebrew ([#1975](https://github.com/swiftlang/vscode-swift/pull/1975))
Expand Down
5 changes: 5 additions & 0 deletions src/WorkspaceContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { CommentCompletionProviders } from "./editor/CommentCompletion";
import { SwiftLogger } from "./logging/SwiftLogger";
import { SwiftLoggerFactory } from "./logging/SwiftLoggerFactory";
import { LanguageClientToolchainCoordinator } from "./sourcekit-lsp/LanguageClientToolchainCoordinator";
import { SourcekitSchemaRegistry } from "./sourcekit-lsp/SourcekitSchemaRegistry";
import { DocCDocumentationRequest, ReIndexProjectRequest } from "./sourcekit-lsp/extensions";
import { SwiftPluginTaskProvider } from "./tasks/SwiftPluginTaskProvider";
import { SwiftTaskProvider } from "./tasks/SwiftTaskProvider";
Expand Down Expand Up @@ -62,6 +63,7 @@ export class WorkspaceContext implements vscode.Disposable {
public documentation: DocumentationManager;
public testRunManager: TestRunManager;
public projectPanel: ProjectPanelProvider;
private sourcekitSchemaRegistry: SourcekitSchemaRegistry;
private lastFocusUri: vscode.Uri | undefined;
private initialisationFinished = false;

Expand Down Expand Up @@ -105,6 +107,7 @@ export class WorkspaceContext implements vscode.Disposable {
this.currentDocument = null;
this.commentCompletionProvider = new CommentCompletionProviders();
this.projectPanel = new ProjectPanelProvider(this);
this.sourcekitSchemaRegistry = new SourcekitSchemaRegistry(this);

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

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

dispose() {
this.sourcekitSchemaRegistry.dispose();
this.folders.forEach(f => f.dispose());
this.folders.length = 0;
this.subscriptions.forEach(item => item.dispose());
Expand Down
32 changes: 30 additions & 2 deletions src/commands/generateSourcekitConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import { FolderContext } from "../FolderContext";
import { WorkspaceContext } from "../WorkspaceContext";
import configuration from "../configuration";
import { selectFolder } from "../ui/SelectFolderQuickPick";
import { fileExists } from "../utilities/filesystem";
import restartLSPServer from "./restartLSPServer";

export const sourcekitDotFolder: string = ".sourcekit-lsp";
export const sourcekitConfigFileName: string = "config.json";
const sourcekitDotFolder: string = ".sourcekit-lsp";
const sourcekitConfigFileName: string = "config.json";

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

export async function determineSchemaURL(folderContext: FolderContext): Promise<string> {
// Check if local schema exists first
if (await hasLocalSchema(folderContext)) {
const localPath = localSchemaPath(folderContext);
return vscode.Uri.file(localPath).toString();
}

// Fall back to remote URL for older toolchains
const version = folderContext.toolchain.swiftVersion;
const versionString = `${version.major}.${version.minor}`;
let branch =
Expand All @@ -138,6 +146,26 @@ export async function determineSchemaURL(folderContext: FolderContext): Promise<
return schemaURL(branch);
}

/**
* Returns the path to the local sourcekit-lsp schema file in the toolchain.
* This file only exists in newer toolchains (approximately Swift 6.3+).
*/
export function localSchemaPath(folderContext: FolderContext): string {
return join(
folderContext.toolchain.toolchainPath,
"share",
"sourcekit-lsp",
"config.schema.json"
);
}

/**
* Checks if the local schema file exists in the toolchain.
*/
export async function hasLocalSchema(folderContext: FolderContext): Promise<boolean> {
return await fileExists(localSchemaPath(folderContext));
}

async function getValidatedFolderContext(
uri: vscode.Uri,
workspaceContext: WorkspaceContext
Expand Down
91 changes: 91 additions & 0 deletions src/sourcekit-lsp/SourcekitSchemaRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//===----------------------------------------------------------------------===//
//
// 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 * as vscode from "vscode";

import { FolderContext } from "../FolderContext";
import { WorkspaceContext } from "../WorkspaceContext";
import {
determineSchemaURL,
sourcekitConfigFilePath,
} from "../commands/generateSourcekitConfiguration";

/**
* Manages dynamic JSON schema associations for sourcekit-lsp config files.
* This allows VS Code to provide validation and IntelliSense using the
* appropriate schema (local or remote) based on the toolchain.
*/
export class SourcekitSchemaRegistry {
private disposables: vscode.Disposable[] = [];

constructor(private workspaceContext: WorkspaceContext) {}

/**
* Registers event handlers to dynamically configure JSON schemas
* for sourcekit-lsp config documents.
*/
register(): vscode.Disposable {
// Handle documents that are already open
vscode.workspace.textDocuments.forEach(doc => {
void this.configureSchemaForDocument(doc);
});

// Handle newly opened documents
const onDidOpenDisposable = vscode.workspace.onDidOpenTextDocument(doc => {
void this.configureSchemaForDocument(doc);
});

this.disposables.push(onDidOpenDisposable);

return vscode.Disposable.from(...this.disposables);
}

/**
* Configures the JSON schema for a document if it's a sourcekit-lsp config file.
*/
private async configureSchemaForDocument(document: vscode.TextDocument): Promise<void> {
if (document.languageId !== "json") {
return;
}

const folderContext = await this.getFolderContextForDocument(document);
if (!folderContext) {
return;
}

const schemaUrl = await determineSchemaURL(folderContext);

// Use VS Code's JSON language configuration API to associate the schema
await vscode.commands.executeCommand("json.setSchema", document.uri.toString(), schemaUrl);
}

/**
* Gets the FolderContext for a document if it's a sourcekit-lsp config file.
*/
private async getFolderContextForDocument(
document: vscode.TextDocument
): Promise<FolderContext | null> {
for (const folderContext of this.workspaceContext.folders) {
const configPath = sourcekitConfigFilePath(folderContext);
if (document.uri.fsPath === configPath) {
return folderContext;
}
}
return null;
}

dispose() {
this.disposables.forEach(d => d.dispose());
this.disposables = [];
}
}
Loading