From 9c31931232a009a732195b70709b14834680a1d7 Mon Sep 17 00:00:00 2001 From: Suyog Habbu Date: Thu, 8 Oct 2026 14:37:26 +0530 Subject: [PATCH 1/4] Add deprecated accessors and show a deprecation notice on hover Registers deprecationMessage and markdownDeprecationMessage in the vscode vocabulary. JsonSchemaAnnotation gets a deprecated accessor for the standard keyword and VscodeAnnotation adds the message accessors, which also deprecate on their own. Hover shows a notice after the description. --- language-server/src/evaluation/Annotation.ts | 7 + .../src/evaluation/JsonSchemaAnnotation.ts | 4 + .../src/evaluation/VscodeAnnotation.ts | 17 ++ language-server/src/features/Hover.test.ts | 170 ++++++++++++++++++ language-server/src/features/Hover.ts | 7 + language-server/src/vocabularies/vscode.ts | 22 ++- 6 files changed, 225 insertions(+), 2 deletions(-) diff --git a/language-server/src/evaluation/Annotation.ts b/language-server/src/evaluation/Annotation.ts index e0978bf..9834df5 100644 --- a/language-server/src/evaluation/Annotation.ts +++ b/language-server/src/evaluation/Annotation.ts @@ -5,3 +5,10 @@ import { VscodeAnnotation } from "./VscodeAnnotation.ts"; // accessors of every vocabulary. A vocabulary's mixin overrides the accessors of // the ones it's applied to, so their order matters. export class Annotation extends VscodeAnnotation(JsonSchemaAnnotation) {} + +// The annotation that deprecates a location or value, preferring one that +// says why. Undefined if none of them deprecate it. +export const findDeprecated = (annotations: Annotation[]): Annotation | undefined => { + const deprecated = annotations.filter((annotation) => annotation.deprecated()); + return deprecated.find((annotation) => annotation.markdownDeprecationMessage() !== undefined) ?? deprecated[0]; +}; diff --git a/language-server/src/evaluation/JsonSchemaAnnotation.ts b/language-server/src/evaluation/JsonSchemaAnnotation.ts index d643b57..93b3760 100644 --- a/language-server/src/evaluation/JsonSchemaAnnotation.ts +++ b/language-server/src/evaluation/JsonSchemaAnnotation.ts @@ -36,6 +36,10 @@ export class JsonSchemaAnnotation { return this.keywords["https://json-schema.org/keyword/description"] as string | undefined; } + deprecated(): boolean { + return this.keywords["https://json-schema.org/keyword/deprecated"] === true; + } + formats(): unknown[] { return Pact.pipe( FORMAT_KEYWORDS, diff --git a/language-server/src/evaluation/VscodeAnnotation.ts b/language-server/src/evaluation/VscodeAnnotation.ts index 6199284..a836094 100644 --- a/language-server/src/evaluation/VscodeAnnotation.ts +++ b/language-server/src/evaluation/VscodeAnnotation.ts @@ -21,6 +21,23 @@ export const VscodeAnnotation = >(Ba ?? super.description()) as string | undefined; } + // A deprecation message deprecates on its own, without the deprecated keyword + override deprecated(): boolean { + return super.deprecated() || this.markdownDeprecationMessage() !== undefined; + } + + deprecationMessage(): string | undefined { + return (this.keywords["https://microsoft.com/keyword/deprecationMessage"] + ?? this.keywords["https://json-schema.org/keyword/unknown#deprecationMessage"]) as string | undefined; + } + + // Prefers markdownDeprecationMessage over deprecationMessage + markdownDeprecationMessage(): string | undefined { + return (this.keywords["https://microsoft.com/keyword/markdownDeprecationMessage"] + ?? this.keywords["https://json-schema.org/keyword/unknown#markdownDeprecationMessage"] + ?? this.deprecationMessage()) as string | undefined; + } + defaultSnippets(): DefaultSnippet[] { return (this.keywords["https://microsoft.com/keyword/defaultSnippets"] ?? this.keywords["https://json-schema.org/keyword/unknown#defaultSnippets"] diff --git a/language-server/src/features/Hover.test.ts b/language-server/src/features/Hover.test.ts index ecbffc9..86336c0 100644 --- a/language-server/src/features/Hover.test.ts +++ b/language-server/src/features/Hover.test.ts @@ -706,4 +706,174 @@ _hyperjump-json-language-server_` } }); }); + + describe("deprecated", () => { + test("shows a deprecation notice for the 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: 18 } + }); + + expect(result).toEqual({ + contents: { + kind: "markdown", + value: `The name. + +⚠️ **Deprecated** + +--- + +_hyperjump-json-language-server_` + } + }); + }); + + test("shows 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("prefers 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_` + } + }); + }); + + test("a deprecated anyOf alternative deprecates the value but not the property", async () => { + fixtureSchemaUri = await client.writeDocument("schema.json", `{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "bar": { + "anyOf": [ + { "const": "a", "deprecationMessage": "Use b." }, + { "const": "b" } + ] + } + } + }`); + + await client.writeDocument("instance.json", `{ + "$schema": "${fixtureSchemaUri}", + "bar": "a" + }`); + const uri = await client.openDocument("instance.json"); + + const keyHover = await client.sendRequest(HoverRequest.type, { + textDocument: { uri }, + position: { line: 2, character: 10 } + }); + expect(keyHover).toBeNull(); + + const valueHover = await client.sendRequest(HoverRequest.type, { + textDocument: { uri }, + position: { line: 2, character: 16 } + }); + expect(valueHover).toEqual({ + contents: { + kind: "markdown", + value: `⚠️ **Deprecated:** Use b. + +--- + +_hyperjump-json-language-server_` + } + }); + }); + }); }); diff --git a/language-server/src/features/Hover.ts b/language-server/src/features/Hover.ts index b93c634..4e74012 100644 --- a/language-server/src/features/Hover.ts +++ b/language-server/src/features/Hover.ts @@ -1,6 +1,7 @@ import { MarkupKind } from "vscode-languageserver"; import { JsonDocuments } from "../services/JsonDocuments.ts"; import { LspEvaluationPlugin } from "../evaluation/LspEvaluationPlugin.ts"; +import { findDeprecated } from "../evaluation/Annotation.ts"; import type { Server } from "../services/Server.ts"; import type { ServerCapabilities } from "vscode-languageserver"; @@ -49,6 +50,12 @@ export class Hover { } } + const deprecated = findDeprecated(annotations); + if (deprecated) { + const message = deprecated.markdownDeprecationMessage(); + lines.push(message ? `⚠️ **Deprecated:** ${message}` : "⚠️ **Deprecated**"); + } + if (lines.length === 0) { return; } diff --git a/language-server/src/vocabularies/vscode.ts b/language-server/src/vocabularies/vscode.ts index 54cd0d4..ef2fb8d 100644 --- a/language-server/src/vocabularies/vscode.ts +++ b/language-server/src/vocabularies/vscode.ts @@ -18,9 +18,25 @@ addKeyword({ annotation: (value: unknown) => value }); +addKeyword({ + id: "https://microsoft.com/keyword/deprecationMessage", + compile: (schema) => Browser.value(schema), + interpret: () => true, + annotation: (value: unknown) => value +}); + +addKeyword({ + id: "https://microsoft.com/keyword/markdownDeprecationMessage", + compile: (schema) => Browser.value(schema), + interpret: () => true, + annotation: (value: unknown) => value +}); + defineVocabulary("https://microsoft.com/vocab/vscode", { markdownDescription: "https://microsoft.com/keyword/markdownDescription", - defaultSnippets: "https://microsoft.com/keyword/defaultSnippets" + defaultSnippets: "https://microsoft.com/keyword/defaultSnippets", + deprecationMessage: "https://microsoft.com/keyword/deprecationMessage", + markdownDeprecationMessage: "https://microsoft.com/keyword/markdownDeprecationMessage" }); registerSchema({ @@ -41,6 +57,8 @@ registerSchema({ bodyText: { type: "string" } } } - } + }, + deprecationMessage: { type: "string" }, + markdownDeprecationMessage: { type: "string" } } }); From b722950b15d4a63e41eabc9f19c4976a0d078e8b Mon Sep 17 00:00:00 2001 From: Suyog Habbu Date: Thu, 8 Oct 2026 14:38:37 +0530 Subject: [PATCH 2/4] Report deprecated locations and values as diagnostics A deprecated property is marked on its key no matter what its value is. A value deprecated by the schema branch that matched it, such as an anyOf alternative, is marked on the value. --- language-server/src/build-server.ts | 4 +- .../DeprecatedDiagnosticsProvider.test.ts | 193 ++++++++++++++++++ .../DeprecatedDiagnosticsProvider.ts | 69 +++++++ 3 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 language-server/src/features/diagnostics/DeprecatedDiagnosticsProvider.test.ts create mode 100644 language-server/src/features/diagnostics/DeprecatedDiagnosticsProvider.ts diff --git a/language-server/src/build-server.ts b/language-server/src/build-server.ts index d06cd07..3f76038 100644 --- a/language-server/src/build-server.ts +++ b/language-server/src/build-server.ts @@ -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"; @@ -41,7 +42,8 @@ export const buildServer = (connection: Connection): Server => { new Diagnostics(server, jsonDocuments, jsonSchema, [ new SyntaxValidationDiagnosticsProvider(server), new SchemaRegistrationDiagnosticsProvider(registry, jsonSchema), - new SchemaValidationDiagnosticsProvider(jsonSchema) + new SchemaValidationDiagnosticsProvider(jsonSchema), + new DeprecatedDiagnosticsProvider(jsonSchema) ]); new Formatting(server, jsonDocuments); diff --git a/language-server/src/features/diagnostics/DeprecatedDiagnosticsProvider.test.ts b/language-server/src/features/diagnostics/DeprecatedDiagnosticsProvider.test.ts new file mode 100644 index 0000000..538050b --- /dev/null +++ b/language-server/src/features/diagnostics/DeprecatedDiagnosticsProvider.test.ts @@ -0,0 +1,193 @@ +import { describe, test, expect, afterEach, beforeEach } from "vitest"; +import { DiagnosticSeverity, DiagnosticTag } from "vscode-languageserver"; +import { TestClient } from "../../test/TestClient.ts"; + +describe("Deprecated Diagnostics", () => { + let client: TestClient; + let fixtureSchemaUri: string; + + beforeEach(async () => { + client = new TestClient(); + await client.start(); + }); + + afterEach(async () => { + await client.stop(); + }); + + test("a deprecated property is reported on its key with the deprecationMessage", async () => { + fixtureSchemaUri = await client.writeDocument("schema.json", `{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { + "type": "string", + "deprecationMessage": "Use fullName instead." + }, + "fullName": { "type": "string" } + } + }`); + + await client.writeDocument("instance.json", `{ + "$schema": "${fixtureSchemaUri}", + "name": "Alice", + "fullName": "Alice Smith" + }`); + const diagnostics = client.getDiagnostics("instance.json"); + await client.openDocument("instance.json"); + + await expect(diagnostics).resolves.toEqual([ + { + severity: DiagnosticSeverity.Warning, + tags: [DiagnosticTag.Deprecated], + range: { + start: { line: 2, character: 6 }, + end: { line: 2, character: 12 } + }, + message: "Use fullName instead.", + source: "hyperjump-json-language-server" + } + ]); + }); + + test("the deprecated keyword uses a default message", async () => { + fixtureSchemaUri = await client.writeDocument("schema.json", `{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { + "type": "string", + "deprecated": true + } + } + }`); + + await client.writeDocument("instance.json", `{ + "$schema": "${fixtureSchemaUri}", + "name": "Alice" + }`); + const diagnostics = client.getDiagnostics("instance.json"); + await client.openDocument("instance.json"); + + await expect(diagnostics).resolves.toEqual([ + expect.objectContaining({ + tags: [DiagnosticTag.Deprecated], + message: "Deprecated" + }) + ]); + }); + + test("a deprecated array item is reported on the item", async () => { + fixtureSchemaUri = await client.writeDocument("schema.json", `{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "anyOf": [ + { "const": "old", "deprecationMessage": "Use new." }, + { "const": "new" } + ] + } + } + } + }`); + + await client.writeDocument("instance.json", `{ + "$schema": "${fixtureSchemaUri}", + "tags": ["new", "old"] + }`); + const diagnostics = client.getDiagnostics("instance.json"); + await client.openDocument("instance.json"); + + await expect(diagnostics).resolves.toEqual([ + expect.objectContaining({ + range: { + start: { line: 2, character: 22 }, + end: { line: 2, character: 27 } + }, + message: "Use new." + }) + ]); + }); + + describe("deprecated locations and deprecated values", () => { + beforeEach(async () => { + fixtureSchemaUri = await client.writeDocument("schema.json", `{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "foo": { + "type": "string", + "deprecated": true + }, + "bar": { + "anyOf": [ + { "const": "a", "deprecated": true }, + { "const": "b" } + ] + }, + "baz": { + "deprecated": true, + "anyOf": [ + { "const": "c", "deprecated": true }, + { "const": "d" } + ] + } + } + }`); + }); + + test("a deprecated location is marked on the key and a deprecated value on the value", async () => { + await client.writeDocument("instance.json", `{ + "$schema": "${fixtureSchemaUri}", + "foo": "x", + "bar": "a", + "baz": "c" + }`); + const diagnostics = client.getDiagnostics("instance.json"); + await client.openDocument("instance.json"); + + await expect(diagnostics).resolves.toEqual([ + expect.objectContaining({ range: { start: { line: 2, character: 6 }, end: { line: 2, character: 11 } } }), + expect.objectContaining({ range: { start: { line: 3, character: 13 }, end: { line: 3, character: 16 } } }), + expect.objectContaining({ range: { start: { line: 4, character: 6 }, end: { line: 4, character: 11 } } }), + expect.objectContaining({ range: { start: { line: 4, character: 13 }, end: { line: 4, character: 16 } } }) + ]); + }); + + test("a value that isn't deprecated isn't marked even if the location or other values are", async () => { + await client.writeDocument("instance.json", `{ + "$schema": "${fixtureSchemaUri}", + "bar": "b", + "baz": "d" + }`); + const diagnostics = client.getDiagnostics("instance.json"); + await client.openDocument("instance.json"); + + await expect(diagnostics).resolves.toEqual([ + expect.objectContaining({ range: { start: { line: 3, character: 6 }, end: { line: 3, character: 11 } } }) + ]); + }); + }); + + test("no diagnostics when nothing is deprecated", async () => { + fixtureSchemaUri = await client.writeDocument("schema.json", `{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + } + }`); + + await client.writeDocument("instance.json", `{ + "$schema": "${fixtureSchemaUri}", + "name": "Alice" + }`); + const diagnostics = client.getDiagnostics("instance.json"); + await client.openDocument("instance.json"); + + await expect(diagnostics).resolves.toHaveLength(0); + }); +}); diff --git a/language-server/src/features/diagnostics/DeprecatedDiagnosticsProvider.ts b/language-server/src/features/diagnostics/DeprecatedDiagnosticsProvider.ts new file mode 100644 index 0000000..9f43b49 --- /dev/null +++ b/language-server/src/features/diagnostics/DeprecatedDiagnosticsProvider.ts @@ -0,0 +1,69 @@ +import { DiagnosticSeverity, DiagnosticTag } from "vscode-languageserver"; +import { LspEvaluationPlugin } from "../../evaluation/LspEvaluationPlugin.ts"; +import { findDeprecated } from "../../evaluation/Annotation.ts"; + +import type { Diagnostic } from "vscode-languageserver"; +import type { Node } from "jsonc-parser"; +import type { DiagnosticsProvider } from "./Diagnostics.ts"; +import type { Annotation } from "../../evaluation/Annotation.ts"; +import type { JsonDocument } from "../../models/JsonDocument.ts"; +import type { JsonSchema } from "../../services/JsonSchema.ts"; + +export class DeprecatedDiagnosticsProvider implements DiagnosticsProvider { + private jsonSchema: JsonSchema; + + constructor(jsonSchema: JsonSchema) { + this.jsonSchema = jsonSchema; + } + + async getDiagnostics(jsonDocument: JsonDocument) { + const diagnostics: Diagnostic[] = []; + + const ast = jsonDocument.findNodeAtPointer(""); + if (!ast) { + return diagnostics; + } + + try { + const result = await this.jsonSchema.validate(jsonDocument); + if (!result) { + return diagnostics; + } + + const plugin = LspEvaluationPlugin.from(result); + + const report = (node: Node, annotation: Annotation) => { + diagnostics.push({ + severity: DiagnosticSeverity.Warning, + tags: [DiagnosticTag.Deprecated], + range: jsonDocument.rangeAt(node.offset, node.offset + node.length), + message: annotation.deprecationMessage() ?? "Deprecated", + source: "hyperjump-json-language-server" + }); + }; + + // A deprecated property is marked on its key no matter what its value + // is. A deprecated value is marked on the value. + jsonDocument.walkNodes(ast, (node) => { + const pointer = jsonDocument.getPointerForNode(node); + const location = findDeprecated(plugin.getLocationAnnotations(pointer)); + const value = findDeprecated(plugin.getValueAnnotations(pointer)); + + if (node.parent?.type === "property") { + if (location) { + report(node.parent.children![0], location); + } + if (value) { + report(node, value); + } + } else if (location ?? value) { + report(node, (location ?? value)!); + } + }); + } catch { + // Schema errors are reported by SchemaValidationDiagnosticsProvider + } + + return diagnostics; + } +} From 6bbc9a12c8db3db8b48f9cf1be0f1da146b8d806 Mon Sep 17 00:00:00 2001 From: Suyog Habbu Date: Thu, 8 Oct 2026 14:38:37 +0530 Subject: [PATCH 3/4] Tag deprecated properties in property completions Uses the location annotations of the candidate property, so a property whose values are only conditionally deprecated isn't tagged. --- .../PropertyCompletionsProvider.test.ts | 62 ++++++++++++++++++- .../PropertyCompletionsProvider.ts | 21 ++++++- 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/language-server/src/features/completions/PropertyCompletionsProvider.test.ts b/language-server/src/features/completions/PropertyCompletionsProvider.test.ts index 2b048a8..6fc7bfa 100644 --- a/language-server/src/features/completions/PropertyCompletionsProvider.test.ts +++ b/language-server/src/features/completions/PropertyCompletionsProvider.test.ts @@ -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", () => { @@ -1182,4 +1182,64 @@ describe("Property completions", () => { expect(completions).toEqual([]); }); + + describe("deprecated", () => { + beforeEach(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." }, + "age": { "type": "integer", "deprecated": true }, + "bar": { + "anyOf": [ + { "const": "a", "deprecated": true }, + { "const": "b" } + ] + }, + "fullName": { "type": "string" } + } + } + } + }`); + + await client.writeDocument("instance.json", `{ + "$schema": "${fixtureSchemaUri}", + "value": { + "" + } + }`); + }); + + test("deprecated properties are tagged with the message as documentation", async () => { + const uri = await client.openDocument("instance.json"); + + const completions = await client.sendRequest(CompletionRequest.type, { + textDocument: { uri }, + position: { line: 3, character: 11 } + }) as CompletionItem[]; + + expect(completions).toMatchObject([ + { label: "name", tags: [CompletionItemTag.Deprecated], documentation: { kind: "markdown", value: "Use fullName instead." } }, + { label: "age", tags: [CompletionItemTag.Deprecated] }, + { label: "bar" }, + { label: "fullName" } + ]); + expect(completions[1].documentation).toBeUndefined(); + }); + + test("a property with a deprecated value isn't deprecated itself", async () => { + const uri = await client.openDocument("instance.json"); + + const completions = await client.sendRequest(CompletionRequest.type, { + textDocument: { uri }, + position: { line: 3, character: 11 } + }) as CompletionItem[]; + + expect(completions[2].tags).toBeUndefined(); + expect(completions[3].tags).toBeUndefined(); + }); + }); }); diff --git a/language-server/src/features/completions/PropertyCompletionsProvider.ts b/language-server/src/features/completions/PropertyCompletionsProvider.ts index f6a4228..0b9832d 100644 --- a/language-server/src/features/completions/PropertyCompletionsProvider.ts +++ b/language-server/src/features/completions/PropertyCompletionsProvider.ts @@ -1,6 +1,8 @@ -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 { LspEvaluationPlugin } from "../../evaluation/LspEvaluationPlugin.ts"; +import { findDeprecated } from "../../evaluation/Annotation.ts"; import type { CompletionItem } from "vscode-languageserver"; import type { CompletionContext, CompletionsProvider } from "./Completions.ts"; @@ -43,7 +45,7 @@ export class PropertyCompletionsProvider implements CompletionsProvider { continue; } - completionItems.push({ + const completionItem: CompletionItem = { label: propertyName, kind: CompletionItemKind.Property, labelDetails: { @@ -55,7 +57,20 @@ export class PropertyCompletionsProvider implements CompletionsProvider { newText: `"${propertyName}": ` }, command: { title: "Suggest", command: "editor.action.triggerSuggest" } - }); + }; + + // A property is deprecated only if it's deprecated no matter what its value is + const propertyLocation = JsonPointer.append(propertyName, instanceLocation); + const deprecated = findDeprecated(plugin.getLocationAnnotations(propertyLocation)); + if (deprecated) { + completionItem.tags = [CompletionItemTag.Deprecated]; + const message = deprecated.markdownDeprecationMessage(); + if (message) { + completionItem.documentation = { kind: MarkupKind.Markdown, value: message }; + } + } + + completionItems.push(completionItem); } } catch { // No completions on schema error From e1fd17e28c5880469266946f8897b5a5bd69838e Mon Sep 17 00:00:00 2001 From: Suyog Habbu Date: Thu, 8 Oct 2026 14:38:37 +0530 Subject: [PATCH 4/4] Tag deprecated values in value completions Uses the annotations of each completion value, so values of a deprecated location aren't tagged unless they're deprecated themselves. --- .../ValueCompletionsProvider.test.ts | 64 ++++++++++++++++++- .../completions/ValueCompletionsProvider.ts | 19 +++++- 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/language-server/src/features/completions/ValueCompletionsProvider.test.ts b/language-server/src/features/completions/ValueCompletionsProvider.test.ts index 6400e2d..78fa423 100644 --- a/language-server/src/features/completions/ValueCompletionsProvider.test.ts +++ b/language-server/src/features/completions/ValueCompletionsProvider.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect, beforeEach, afterEach } from "vitest"; -import { CompletionItem, CompletionItemKind, CompletionRequest, InsertTextFormat } from "vscode-languageserver"; +import { CompletionItem, CompletionItemKind, CompletionItemTag, CompletionRequest, InsertTextFormat } from "vscode-languageserver"; import { TestClient } from "../../test/TestClient.ts"; describe("Value Completions", () => { @@ -4333,4 +4333,66 @@ describe("Value Completions", () => { expect(completions).toEqual([]); }); + + describe("deprecated", () => { + test("a deprecated anyOf alternative tags its value", async () => { + const fixtureSchemaUri = await client.writeDocument("schema.json", `{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "value": { + "anyOf": [ + { "const": "a", "deprecationMessage": "Use b." }, + { "const": "b" } + ] + } + } + }`); + + await client.writeDocument("instance.json", `{ + "$schema": "${fixtureSchemaUri}", + "value": + }`); + const uri = await client.openDocument("instance.json"); + + const completions = await client.sendRequest(CompletionRequest.type, { + textDocument: { uri }, + position: { line: 2, character: 16 } + }) as CompletionItem[]; + + expect(completions).toMatchObject([ + { label: `"a"`, tags: [CompletionItemTag.Deprecated], documentation: { kind: "markdown", value: "Use b." } }, + { label: `"b"` } + ]); + expect(completions[1].tags).toBeUndefined(); + }); + + test("values of a deprecated location aren't deprecated themselves", async () => { + const fixtureSchemaUri = await client.writeDocument("schema.json", `{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "value": { + "enum": ["a", "b"], + "deprecated": true + } + } + }`); + + await client.writeDocument("instance.json", `{ + "$schema": "${fixtureSchemaUri}", + "value": + }`); + const uri = await client.openDocument("instance.json"); + + const completions = await client.sendRequest(CompletionRequest.type, { + textDocument: { uri }, + position: { line: 2, character: 16 } + }) as CompletionItem[]; + + expect(completions).toMatchObject([{ label: `"a"` }, { label: `"b"` }]); + expect(completions[0].tags).toBeUndefined(); + expect(completions[1].tags).toBeUndefined(); + }); + }); }); diff --git a/language-server/src/features/completions/ValueCompletionsProvider.ts b/language-server/src/features/completions/ValueCompletionsProvider.ts index 6d304e2..21da1a1 100644 --- a/language-server/src/features/completions/ValueCompletionsProvider.ts +++ b/language-server/src/features/completions/ValueCompletionsProvider.ts @@ -1,6 +1,7 @@ -import { CompletionItemKind, InsertTextFormat } from "vscode-languageserver"; +import { CompletionItemKind, CompletionItemTag, InsertTextFormat, MarkupKind } from "vscode-languageserver"; import { JsonDocument } from "../../models/JsonDocument.ts"; import { LspEvaluationPlugin } from "../../evaluation/LspEvaluationPlugin.ts"; +import { findDeprecated } from "../../evaluation/Annotation.ts"; import type { CompletionContext, CompletionsProvider } from "./Completions.ts"; import type { CompletionItem } from "vscode-languageserver"; @@ -33,7 +34,7 @@ export class ValueCompletionsProvider implements CompletionsProvider { const label = completion.kind === "value" ? completion.value : typeSnippets[completion.type].label; const snippet = completion.kind === "value" ? completion.value : typeSnippets[completion.type].snippet; - completions.push({ + const completionItem: CompletionItem = { label, kind: CompletionItemKind.Value, labelDetails: { @@ -44,7 +45,19 @@ export class ValueCompletionsProvider implements CompletionsProvider { range: context.range, newText: /^[:,]$/.test(jsonDocument.getText()[cursorOffset - 1]) ? ` ${snippet}` : snippet } - }); + }; + + // A value is deprecated only because of what it is, not because its location is + const deprecated = findDeprecated(completion.annotations); + if (deprecated) { + completionItem.tags = [CompletionItemTag.Deprecated]; + const message = deprecated.markdownDeprecationMessage(); + if (message) { + completionItem.documentation = { kind: MarkupKind.Markdown, value: message }; + } + } + + completions.push(completionItem); } } catch { // No completions on schema error