Skip to content

Commit 5ff0c38

Browse files
diyaayayjdesrosiers
authored andcommitted
Support Custom Dialects
1 parent 8279d06 commit 5ff0c38

File tree

4 files changed

+165
-13
lines changed

4 files changed

+165
-13
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
2+
import {
3+
DidChangeWatchedFilesNotification,
4+
PublishDiagnosticsNotification,
5+
WorkDoneProgress,
6+
WorkDoneProgressCreateRequest
7+
} from "vscode-languageserver";
8+
import { resolveIri } from "@hyperjump/uri";
9+
import { rm } from "node:fs/promises";
10+
import { fileURLToPath } from "node:url";
11+
import { TestClient } from "../test-client.js";
12+
import documentSettings from "./document-settings.js";
13+
import semanticTokens from "./semantic-tokens.js";
14+
import schemaRegistry from "./schema-registry.js";
15+
import workspace from "./workspace.js";
16+
import validationErrorsFeature from "./validation-errors.js";
17+
import { setupWorkspace, tearDownWorkspace } from "../test-utils.js";
18+
19+
import type { Diagnostic } from "vscode-languageserver";
20+
import type { DocumentSettings } from "./document-settings.js";
21+
22+
23+
describe("Feature - Custom Dialects", () => {
24+
let client: TestClient<DocumentSettings>;
25+
let workspaceFolder: string;
26+
let documentUriB: string;
27+
let documentUri: string;
28+
29+
beforeEach(async () => {
30+
client = new TestClient([
31+
workspace,
32+
documentSettings,
33+
semanticTokens,
34+
schemaRegistry,
35+
validationErrorsFeature
36+
]);
37+
workspaceFolder = await setupWorkspace({
38+
"subjectB.schema.json": `{
39+
"$id": "https://example.com/my-dialect",
40+
"$schema": "https://json-schema.org/draft/2020-12/schema",
41+
42+
"$vocabulary": {
43+
"https://json-schema.org/draft/2020-12/vocab/core": true,
44+
"https://json-schema.org/draft/2020-12/vocab/applicator": true,
45+
"https://json-schema.org/draft/2020-12/vocab/validation": true
46+
}
47+
}`,
48+
"subject.schema.json": `{
49+
"$schema": "https://example.com/my-dialect"
50+
}`
51+
});
52+
documentUriB = resolveIri("./subjectB.schema.json", `${workspaceFolder}/`);
53+
documentUri = resolveIri("./subject.schema.json", `${workspaceFolder}/`);
54+
55+
await client.start({
56+
workspaceFolders: [
57+
{
58+
name: "root",
59+
uri: workspaceFolder
60+
}
61+
]
62+
});
63+
});
64+
65+
afterEach(async () => {
66+
await client.stop();
67+
await tearDownWorkspace(workspaceFolder);
68+
});
69+
70+
test("Registered dialect schema", async () => {
71+
const diagnosticsPromise = new Promise<Diagnostic[]>((resolve) => {
72+
client.onNotification(PublishDiagnosticsNotification.type, (params) => {
73+
resolve(params.diagnostics);
74+
});
75+
});
76+
77+
await client.openDocument(documentUriB);
78+
await client.openDocument(documentUri);
79+
80+
const diagnostics = await diagnosticsPromise;
81+
expect(diagnostics).to.eql([]);
82+
});
83+
84+
test("Unregister dialect schema", async () => {
85+
const diagnosticsPromise = new Promise<Diagnostic[]>((resolve) => {
86+
let diagnostics: Diagnostic[];
87+
88+
client.onRequest(WorkDoneProgressCreateRequest.type, ({ token }) => {
89+
client.onProgress(WorkDoneProgress.type, token, ({ kind }) => {
90+
if (kind === "end") {
91+
resolve(diagnostics);
92+
}
93+
});
94+
});
95+
96+
client.onNotification(PublishDiagnosticsNotification.type, (params) => {
97+
diagnostics = params.diagnostics;
98+
});
99+
});
100+
101+
await rm(fileURLToPath(documentUriB));
102+
await client.sendNotification(DidChangeWatchedFilesNotification.type, {
103+
changes: []
104+
});
105+
106+
const diagnostics = await diagnosticsPromise;
107+
expect(diagnostics[0]?.message).to.eql("Unknown dialect");
108+
});
109+
});

language-server/src/features/hover.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ describe("Feature - Hover", () => {
196196
["type", "\"object\""]
197197
])("%s should have a message", async (keyword, value) => {
198198
documentUri = await client.openDocument("./subject.schema.json", `{
199-
"$schema": "https://json-schema.org/draft/2020-12/schema",
199+
"$schema": "https://json-schema.org/draft/2020-12/schema",${keyword === "$vocabulary" ? `"$id": "https://example.com/schema",` : ""}
200200
"${keyword}": ${value}
201201
}`);
202202

@@ -317,7 +317,7 @@ describe("Feature - Hover", () => {
317317
["type", "\"object\""]
318318
])("%s should have a message", async (keyword, value) => {
319319
documentUri = await client.openDocument("./subject.schema.json", `{
320-
"$schema": "https://json-schema.org/draft/2019-09/schema",
320+
"$schema": "https://json-schema.org/draft/2019-09/schema",${keyword === "$vocabulary" ? `"$id": "https://example.com/schema",` : ""}
321321
"${keyword}": ${value}
322322
}`);
323323

language-server/src/features/semantic-tokens.test.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ describe("Feature - Semantic Tokens", () => {
114114
await client.closeDocument(documentUri);
115115
});
116116

117-
118117
test.each([
119118
// Applicators
120119
["prefixItems", "[{}]", [1, 2, 9, 1, 0, 1, 2, 13, 1, 0]],
@@ -144,7 +143,7 @@ describe("Feature - Semantic Tokens", () => {
144143
["$ref", "\"\"", [1, 2, 9, 1, 0, 1, 2, 6, 1, 0]],
145144
["$dynamicRef", "\"\"", [1, 2, 9, 1, 0, 1, 2, 13, 1, 0]],
146145
["$dynamicAnchor", "\"foo\"", [1, 2, 9, 1, 0, 1, 2, 16, 1, 0]],
147-
["$vocabulary", "{}", [1, 2, 9, 1, 0, 1, 2, 13, 1, 0]],
146+
["$vocabulary", "{}", [1, 2, 9, 1, 0, 0, 58, 5, 1, 0, 1, 2, 13, 1, 0]],
148147
["$comment", "\"\"", [1, 2, 9, 1, 0, 1, 2, 14, 2, 0]],
149148
["$defs", "{}", [1, 2, 9, 1, 0, 1, 2, 7, 1, 0]],
150149

@@ -187,7 +186,7 @@ describe("Feature - Semantic Tokens", () => {
187186
["type", "\"object\"", [1, 2, 9, 1, 0, 1, 2, 6, 1, 0]]
188187
])("%s should be highlighted", async (keyword, value, expected) => {
189188
documentUri = await client.openDocument("./subject.schema.json", `{
190-
"$schema": "https://json-schema.org/draft/2020-12/schema",
189+
"$schema": "https://json-schema.org/draft/2020-12/schema",${keyword === "$vocabulary" ? `"$id": "https://example.com/schema",` : ""}
191190
"${keyword}": ${value}
192191
}`);
193192

@@ -261,7 +260,7 @@ describe("Feature - Semantic Tokens", () => {
261260
["$ref", "\"\"", [1, 2, 9, 1, 0, 1, 2, 6, 1, 0]],
262261
["$recursiveRef", "\"\"", [1, 2, 9, 1, 0, 1, 2, 15, 1, 0]],
263262
["$recursiveAnchor", "true", [1, 2, 9, 1, 0, 1, 2, 18, 1, 0]],
264-
["$vocabulary", "{}", [1, 2, 9, 1, 0, 1, 2, 13, 1, 0]],
263+
["$vocabulary", "{}", [1, 2, 9, 1, 0, 0, 58, 5, 1, 0, 1, 2, 13, 1, 0]],
265264
["$comment", "\"\"", [1, 2, 9, 1, 0, 1, 2, 14, 2, 0]],
266265
["$defs", "{}", [1, 2, 9, 1, 0, 1, 2, 7, 1, 0]],
267266

@@ -300,7 +299,7 @@ describe("Feature - Semantic Tokens", () => {
300299
["type", "\"object\"", [1, 2, 9, 1, 0, 1, 2, 6, 1, 0]]
301300
])("%s should be highlighted", async (keyword, value, expected) => {
302301
documentUri = await client.openDocument("./subject.schema.json", `{
303-
"$schema": "https://json-schema.org/draft/2019-09/schema",
302+
"$schema": "https://json-schema.org/draft/2019-09/schema",${keyword === "$vocabulary" ? `"$id": "https://example.com/schema",` : ""}
304303
"${keyword}": ${value}
305304
}`);
306305

language-server/src/features/workspace.js

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@ import {
1111
} from "vscode-languageserver";
1212
import { TextDocument } from "vscode-languageserver-textdocument";
1313
import { URI } from "vscode-uri";
14+
import { registerSchema, unregisterSchema } from "@hyperjump/json-schema/draft-2020-12";
15+
import { hasDialect } from "@hyperjump/json-schema/experimental";
16+
import { toAbsoluteIri } from "@hyperjump/uri";
17+
import picomatch from "picomatch";
1418
import { publishAsync, subscribe, unsubscribe } from "../pubsub.js";
19+
import * as SchemaNode from "../schema-node.js";
20+
import { keywordNameFor } from "../util.js";
1521
import { allSchemaDocuments, getSchemaDocument } from "./schema-registry.js";
1622
import { getDocumentSettings } from "./document-settings.js";
17-
import picomatch from "picomatch";
1823

1924
/**
2025
* @import { FSWatcher, WatchEventType } from "node:fs"
@@ -33,30 +38,64 @@ import picomatch from "picomatch";
3338
* tags?: DiagnosticTag[];
3439
* }} ValidationDiagnostic
3540
*/
41+
3642
let hasWorkspaceFolderCapability = false;
3743
let hasWorkspaceWatchCapability = false;
3844

3945
/** @type string */
4046
let subscriptionToken;
4147

48+
/** @type Set<string> */
49+
const customDialects = new Set();
50+
4251
/** @type Feature */
4352
export default {
4453
load(connection, documents) {
4554
subscriptionToken = subscribe("workspaceChanged", async (_message, _changes) => {
4655
const reporter = await connection.window.createWorkDoneProgress();
4756
reporter.begin("JSON Schema: Indexing workspace");
4857

58+
// Unregister all existing schemas
59+
for (const dialectUri of customDialects) {
60+
unregisterSchema(dialectUri);
61+
}
62+
customDialects.clear();
63+
4964
// Load all schemas
5065
const settings = await getDocumentSettings(connection);
5166
const schemaFilePatterns = settings.schemaFilePatterns;
5267
for await (const uri of workspaceSchemas(schemaFilePatterns)) {
53-
let textDocument = documents.get(uri);
54-
if (!textDocument) {
55-
const instanceJson = await readFile(fileURLToPath(uri), "utf8");
56-
textDocument = TextDocument.create(uri, "json", -1, instanceJson);
68+
const instanceJson = await readFile(fileURLToPath(uri), "utf8");
69+
const textDocument = TextDocument.create(uri, "json", -1, instanceJson);
70+
71+
const schemaDocument = await getSchemaDocument(connection, textDocument);
72+
for (const schemaResource of schemaDocument.schemaResources) {
73+
const vocabToken = keywordNameFor("https://json-schema.org/keyword/vocabulary", schemaResource.dialectUri);
74+
const vocabularyNode = vocabToken && SchemaNode.step(vocabToken, schemaResource);
75+
if (vocabularyNode) {
76+
registerSchema(SchemaNode.value(schemaResource), schemaResource.baseUri);
77+
customDialects.add(schemaResource.baseUri);
78+
}
5779
}
80+
}
5881

59-
await getSchemaDocument(connection, textDocument);
82+
// Rebuild custom dialect schemas
83+
for (const schemaDocument of allSchemaDocuments()) {
84+
for (const error of schemaDocument.errors) {
85+
try {
86+
const dialectUri = toAbsoluteIri(SchemaNode.value(error.instanceNode));
87+
if (error.keyword === "https://json-schema.org/keyword/schema" && hasDialect(dialectUri)) {
88+
for (const schemaResource of schemaDocument.schemaResources) {
89+
if (customDialects.has(schemaResource.baseUri)) {
90+
unregisterSchema(schemaResource.baseUri);
91+
}
92+
}
93+
await getSchemaDocument(connection, schemaDocument.textDocument);
94+
}
95+
} catch (error) {
96+
// Ignore Invalid IRI for now
97+
}
98+
}
6099
}
61100

62101
// Re/validate all schemas
@@ -176,6 +215,11 @@ export default {
176215
onShutdown() {
177216
removeWorkspaceFolders([...workspaceFolders]);
178217

218+
for (const dialectUri of customDialects) {
219+
unregisterSchema(dialectUri);
220+
}
221+
customDialects.clear();
222+
179223
unsubscribe("workspaceChanged", subscriptionToken);
180224
}
181225
};

0 commit comments

Comments
 (0)