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: 3 additions & 1 deletion language-server/src/build-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Diagnostics } from "./features/diagnostics/Diagnostics.ts";
import { SyntaxValidationDiagnosticsProvider } from "./features/diagnostics/SyntaxValidationDiagnosticsProvider.ts";
import { SchemaValidationDiagnosticsProvider } from "./features/diagnostics/SchemaValidationDiagnosticsProvider.ts";
import { SchemaRegistrationDiagnosticsProvider } from "./features/diagnostics/SchemaRegistrationDiagnosticsProvider.ts";
import { DeprecatedDiagnosticsProvider } from "./features/diagnostics/DeprecatedDiagnosticsProvider.ts";
import { Formatting } from "./features/Formatting.ts";
import { Hover } from "./features/Hover.ts";
import { Completions } from "./features/completions/Completions.ts";
Expand Down Expand Up @@ -37,7 +38,8 @@ export const buildServer = (connection: Connection): Server => {
new Diagnostics(server, jsonDocuments, jsonSchema, [
new SyntaxValidationDiagnosticsProvider(),
new SchemaRegistrationDiagnosticsProvider(registry, jsonSchema),
new SchemaValidationDiagnosticsProvider(jsonSchema)
new SchemaValidationDiagnosticsProvider(jsonSchema),
new DeprecatedDiagnosticsProvider(jsonSchema)
]);

new Formatting(server, jsonDocuments);
Expand Down
4 changes: 4 additions & 0 deletions language-server/src/features/AnnotationsEvaluationPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getKeyword } from "@hyperjump/json-schema/experimental";
import * as Instance from "@hyperjump/json-schema/instance/experimental";
import * as JsonPointer from "@hyperjump/json-pointer";

import type { EvaluationPlugin, ValidationContext } from "@hyperjump/json-schema/experimental";
Expand Down Expand Up @@ -43,6 +44,9 @@ export class AnnotationsEvaluationPlugin implements EvaluationPlugin {
if (this.incompleteLocations.has(pointer)) {
this.recordBuiltAnnotation(pointer, properties[propertyName], schemaContext);
context.evaluatedProperties?.add(propertyName);
} else if (Instance.typeOf(instance) === "object" && !Instance.has(propertyName, instance)) {
// Absent properties are property completion candidates
this.recordBuiltAnnotation(pointer, properties[propertyName], schemaContext);
}
}
break;
Expand Down
29 changes: 29 additions & 0 deletions language-server/src/features/Deprecation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
type Annotation = Record<string, unknown>;

export type Deprecation = {
message?: string;
markdownMessage?: string;
};

// A location is deprecated if any matching schema says so. A branch with a
// message wins over a bare `deprecated: true`.
export const getDeprecation = (annotations: Annotation[]): Deprecation | undefined => {
let deprecation: Deprecation | undefined;

for (const annotation of annotations) {
const message = (annotation["https://microsoft.com/keyword/deprecationMessage"]
?? annotation["https://json-schema.org/keyword/unknown#deprecationMessage"]) as string | undefined;
const markdownMessage = (annotation["https://microsoft.com/keyword/markdownDeprecationMessage"]
?? annotation["https://json-schema.org/keyword/unknown#markdownDeprecationMessage"]) as string | undefined;

if (message !== undefined || markdownMessage !== undefined) {
return { message, markdownMessage };
}

if (annotation["https://json-schema.org/keyword/deprecated"] === true) {
deprecation = {};
}
}

return deprecation;
};
126 changes: 126 additions & 0 deletions language-server/src/features/Hover.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,132 @@ _hyperjump-json-language-server_`

---

_hyperjump-json-language-server_`
}
});
});

test("should show a deprecation notice for the standard deprecated keyword", async () => {
fixtureSchemaUri = await client.writeDocument("schema.json", `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": {
"description": "The name.",
"deprecated": true,
"type": "string"
}
}
}`);

await client.writeDocument("instance.json", `{
"$schema": "${fixtureSchemaUri}",
"name": "Alice"
}`);
const uri = await client.openDocument("instance.json");

const result = await client.sendRequest(HoverRequest.type, {
textDocument: { uri },
position: { line: 2, character: 10 }
});

expect(result).toEqual({
contents: {
kind: "markdown",
value: `The name.

⚠️ **Deprecated**

---

_hyperjump-json-language-server_`
}
});
});

test("should show deprecationMessage as an unknown keyword", async () => {
fixtureSchemaUri = await client.writeDocument("schema.json", `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": {
"deprecationMessage": "Use fullName instead.",
"type": "string"
}
}
}`);

await client.writeDocument("instance.json", `{
"$schema": "${fixtureSchemaUri}",
"name": "Alice"
}`);
const uri = await client.openDocument("instance.json");

const result = await client.sendRequest(HoverRequest.type, {
textDocument: { uri },
position: { line: 2, character: 10 }
});

expect(result).toEqual({
contents: {
kind: "markdown",
value: `⚠️ **Deprecated:** Use fullName instead.

---

_hyperjump-json-language-server_`
}
});
});

test("should prefer markdownDeprecationMessage from a dialect that includes the vscode vocabulary", async () => {
await client.writeDocument("meta-schema.json", `{
"$id": "https://example.com/dialect/vscode",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/core": true,
"https://json-schema.org/draft/2020-12/vocab/applicator": true,
"https://json-schema.org/draft/2020-12/vocab/validation": true,
"https://json-schema.org/draft/2020-12/vocab/meta-data": true,
"https://microsoft.com/vocab/vscode": true
},
"$dynamicAnchor": "meta",
"allOf": [
{ "$ref": "https://json-schema.org/draft/2020-12/schema" },
{ "$ref": "https://microsoft.com/meta/vscode" }
]
}`);

fixtureSchemaUri = await client.writeDocument("schema.json", `{
"$schema": "https://example.com/dialect/vscode",
"type": "object",
"properties": {
"name": {
"deprecationMessage": "Use fullName instead.",
"markdownDeprecationMessage": "Use \`fullName\` instead.",
"type": "string"
}
}
}`);

await client.writeDocument("instance.json", `{
"$schema": "${fixtureSchemaUri}",
"name": "Alice"
}`);
const uri = await client.openDocument("instance.json");

const result = await client.sendRequest(HoverRequest.type, {
textDocument: { uri },
position: { line: 2, character: 10 }
});

expect(result).toEqual({
contents: {
kind: "markdown",
value: `⚠️ **Deprecated:** Use \`fullName\` instead.

---

_hyperjump-json-language-server_`
}
});
Expand Down
7 changes: 7 additions & 0 deletions language-server/src/features/Hover.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { MarkupKind } from "vscode-languageserver";
import { JsonDocuments } from "../services/JsonDocuments.ts";
import { AnnotationsEvaluationPlugin } from "./AnnotationsEvaluationPlugin.ts";
import { getDeprecation } from "./Deprecation.ts";

import type { Server } from "../services/Server.ts";
import type { ServerCapabilities } from "vscode-languageserver";
Expand Down Expand Up @@ -44,6 +45,12 @@ export class Hover {
}
}

const deprecation = getDeprecation(annotations);
if (deprecation) {
const message = deprecation.markdownMessage ?? deprecation.message;
lines.push(message ? `⚠️ **Deprecated:** ${message}` : "⚠️ **Deprecated**");
}

if (lines.length === 0) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, test, expect, beforeEach, afterEach } from "vitest";
import { CompletionItem, CompletionItemKind, CompletionRequest } from "vscode-languageserver";
import { CompletionItem, CompletionItemKind, CompletionItemTag, CompletionRequest } from "vscode-languageserver";
import { TestClient } from "../../test/TestClient.ts";

describe("Property completions", () => {
Expand Down Expand Up @@ -1182,4 +1182,79 @@ describe("Property completions", () => {

expect(completions).toEqual([]);
});

test("deprecated properties are tagged with the deprecation message as documentation", async () => {
fixtureSchemaUri = await client.writeDocument("schema.json", `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"value": {
"properties": {
"name": { "type": "string", "deprecationMessage": "Use fullName instead." },
"fullName": { "type": "string" }
}
}
}
}`);

const instanceText = `{
"$schema": "${fixtureSchemaUri}",
"value": {
""
}
}`;

await client.writeDocument("instance.json", instanceText);
const uri = await client.openDocument("instance.json");

const completions = await client.sendRequest(CompletionRequest.type, {
textDocument: { uri },
position: { line: 3, character: 9 }
}) as CompletionItem[];

expect(completions).toMatchObject([
{
label: "name",
tags: [CompletionItemTag.Deprecated],
documentation: { kind: "markdown", value: "Use fullName instead." }
},
{ label: "fullName" }
]);
expect(completions[1].tags).toBeUndefined();
expect(completions[1].documentation).toBeUndefined();
});

test("properties marked with the standard deprecated keyword are tagged", async () => {
fixtureSchemaUri = await client.writeDocument("schema.json", `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"value": {
"properties": {
"name": { "type": "string", "deprecated": true }
}
}
}
}`);

const instanceText = `{
"$schema": "${fixtureSchemaUri}",
"value": {
""
}
}`;

await client.writeDocument("instance.json", instanceText);
const uri = await client.openDocument("instance.json");

const completions = await client.sendRequest(CompletionRequest.type, {
textDocument: { uri },
position: { line: 3, character: 9 }
}) as CompletionItem[];

expect(completions).toMatchObject([
{ label: "name", tags: [CompletionItemTag.Deprecated] }
]);
expect(completions[0].documentation).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { CompletionItemKind } from "vscode-languageserver";
import { CompletionItemKind, CompletionItemTag, MarkupKind } from "vscode-languageserver";
import * as JsonPointer from "@hyperjump/json-pointer";
import * as Pact from "@hyperjump/pact";
import { CompletionsEvaluationPlugin } from "./CompletionsEvaluationPlugin.ts";
import { AnnotationsEvaluationPlugin } from "../AnnotationsEvaluationPlugin.ts";
import { getDeprecation } from "../Deprecation.ts";

import type { CompletionItem } from "vscode-languageserver";
import type { CompletionContext, CompletionsProvider } from "./Completions.ts";
Expand Down Expand Up @@ -33,13 +36,14 @@ export class PropertyCompletionsProvider implements CompletionsProvider {
try {
const result = await this.jsonSchema.validate(jsonDocument);
const plugin = result.plugins.get(CompletionsEvaluationPlugin.id) as CompletionsEvaluationPlugin;
const annotationsPlugin = result.plugins.get(AnnotationsEvaluationPlugin.id) as AnnotationsEvaluationPlugin;

for (const propertyName of plugin.getPropertyCompletions(instanceLocation)) {
if (existingPropertyNames.has(propertyName)) {
continue;
}

completionItems.push({
const completionItem: CompletionItem = {
label: propertyName,
kind: CompletionItemKind.Property,
labelDetails: {
Expand All @@ -51,7 +55,19 @@ export class PropertyCompletionsProvider implements CompletionsProvider {
newText: `"${propertyName}": `
},
command: { title: "Suggest", command: "editor.action.triggerSuggest" }
});
};

const propertyLocation = JsonPointer.append(propertyName, instanceLocation);
const deprecation = getDeprecation(annotationsPlugin.getAnnotations(propertyLocation));
if (deprecation) {
completionItem.tags = [CompletionItemTag.Deprecated];
const message = deprecation.markdownMessage ?? deprecation.message;
if (message) {
completionItem.documentation = { kind: MarkupKind.Markdown, value: message };
}
}

completionItems.push(completionItem);
}
} catch {
// No completions on schema error
Expand Down
Loading
Loading