From a474602a12570b9fe33d2b49e896c9782fea8177 Mon Sep 17 00:00:00 2001 From: Diya Date: Tue, 30 Jul 2024 23:42:50 +0530 Subject: [PATCH] added goto-references --- language-server/src/features/hover.test.ts | 21 +- .../src/features/if-then-completion.test.ts | 3 +- language-server/src/features/references.js | 80 +++++++ .../src/features/references.test.ts | 203 ++++++++++++++++++ .../src/features/schema-completion.test.ts | 3 +- .../src/features/semantic-tokens.test.ts | 8 +- .../src/features/validate-references.js | 2 +- language-server/src/server.js | 2 + language-server/src/test-client.ts | 5 +- 9 files changed, 311 insertions(+), 16 deletions(-) create mode 100644 language-server/src/features/references.js create mode 100644 language-server/src/features/references.test.ts diff --git a/language-server/src/features/hover.test.ts b/language-server/src/features/hover.test.ts index ed71d62..01a63d0 100644 --- a/language-server/src/features/hover.test.ts +++ b/language-server/src/features/hover.test.ts @@ -1,7 +1,8 @@ -import { afterAll, beforeAll, describe, expect, test } from "vitest"; +import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest"; import { HoverRequest, MarkupKind } from "vscode-languageserver"; import { TestClient } from "../test-client.js"; import hover from "./hover.js"; +import schemaRegistry from "./schema-registry.js"; import type { Hover, MarkupContent } from "vscode-languageserver"; import type { DocumentSettings } from "./document-settings.js"; @@ -11,7 +12,7 @@ describe("Feature - Hover", () => { let client: TestClient; beforeAll(async () => { - client = new TestClient([hover]); + client = new TestClient([schemaRegistry, hover]); await client.start(); }); @@ -41,7 +42,7 @@ describe("Feature - Hover", () => { }); }); - afterAll(async () => { + afterEach(async () => { await client.closeDocument(documentUri); }); @@ -77,7 +78,7 @@ describe("Feature - Hover", () => { }); }); - afterAll(async () => { + afterEach(async () => { await client.closeDocument(documentUri); }); @@ -107,7 +108,7 @@ describe("Feature - Hover", () => { }); }); - afterAll(async () => { + afterEach(async () => { await client.closeDocument(documentUri); }); @@ -119,7 +120,7 @@ describe("Feature - Hover", () => { describe("2020-12", () => { let documentUri: string; - afterAll(async () => { + afterEach(async () => { await client.closeDocument(documentUri); }); @@ -242,7 +243,7 @@ describe("Feature - Hover", () => { describe("2019-09", () => { let documentUri: string; - afterAll(async () => { + afterEach(async () => { await client.closeDocument(documentUri); }); @@ -363,7 +364,7 @@ describe("Feature - Hover", () => { describe("draft-07", () => { let documentUri: string; - afterAll(async () => { + afterEach(async () => { await client.closeDocument(documentUri); }); @@ -466,7 +467,7 @@ describe("Feature - Hover", () => { describe("draft-06", () => { let documentUri: string; - afterAll(async () => { + afterEach(async () => { await client.closeDocument(documentUri); }); @@ -564,7 +565,7 @@ describe("Feature - Hover", () => { describe("draft-04", () => { let documentUri: string; - afterAll(async () => { + afterEach(async () => { await client.closeDocument(documentUri); }); diff --git a/language-server/src/features/if-then-completion.test.ts b/language-server/src/features/if-then-completion.test.ts index 8236cae..9d169e7 100644 --- a/language-server/src/features/if-then-completion.test.ts +++ b/language-server/src/features/if-then-completion.test.ts @@ -3,6 +3,7 @@ import { CompletionRequest } from "vscode-languageserver"; import { TestClient } from "../test-client.js"; import completion from "./completion.js"; import ifThenCompletionFeature, { ifThenPatternCompletion } from "./if-then-completion.js"; +import schemaRegistry from "./schema-registry.js"; import type { DocumentSettings } from "./document-settings.js"; @@ -12,7 +13,7 @@ describe("Feature - if/then completion", () => { let documentUri: string; beforeAll(async () => { - client = new TestClient([completion, ifThenCompletionFeature]); + client = new TestClient([schemaRegistry, completion, ifThenCompletionFeature]); await client.start(); }); diff --git a/language-server/src/features/references.js b/language-server/src/features/references.js new file mode 100644 index 0000000..5c8a849 --- /dev/null +++ b/language-server/src/features/references.js @@ -0,0 +1,80 @@ +import * as SchemaDocument from "../schema-document.js"; +import * as SchemaNode from "../schema-node.js"; +import { getSchemaDocument, allSchemaDocuments } from "./schema-registry.js"; +import { references } from "./validate-references.js"; + +/** @import { Feature } from "../build-server.js" */ +/** @import { SchemaNode as SchemaNodeType } from "../schema-node.js" */ + +/** @type Feature */ +export default { + load(connection, documents) { + const highlightBlockDialects = new Set([ + "http://json-schema.org/draft-04/schema", + "http://json-schema.org/draft-06/schema", + "http://json-schema.org/draft-07/schema" + ]); + const shouldHighlightBlock = (/** @type {string | undefined} */ uri) => { + if (uri === undefined) { + return false; + } + return highlightBlockDialects.has(uri); + }; + + connection.onReferences(async ({ textDocument, position }) => { + const document = documents.get(textDocument.uri); + if (!document) { + return []; + } + + const schemaDocument = await getSchemaDocument(connection, document); + const offset = document.offsetAt(position); + const node = SchemaDocument.findNodeAtOffset(schemaDocument, offset); + + if (!node) { + return []; + } + + const targetSchemaUri = SchemaNode.uri(node); + const schemaReferences = []; + + for (const schemaDocument of allSchemaDocuments()) { + for (const schemaResource of schemaDocument.schemaResources) { + for (const referenceNode of references(schemaResource)) { + const reference = SchemaNode.value(referenceNode); + const referencedSchema = SchemaNode.get(reference, schemaResource); + if (!referencedSchema) { + continue; + } + + if (SchemaNode.uri(referencedSchema) === targetSchemaUri) { + const hightlightNode = /** @type SchemaNodeType */ (shouldHighlightBlock(referenceNode.dialectUri) + ? referenceNode.parent?.parent + : referenceNode); + schemaReferences.push({ + uri: schemaDocument.textDocument.uri, + range: { + start: schemaDocument.textDocument.positionAt(hightlightNode.offset), + end: schemaDocument.textDocument.positionAt(hightlightNode.offset + hightlightNode.textLength) + } + }); + } + } + } + } + return schemaReferences; + }); + }, + + onInitialize() { + return { + referencesProvider: true + }; + }, + + async onInitialized() { + }, + + onShutdown() { + } +}; diff --git a/language-server/src/features/references.test.ts b/language-server/src/features/references.test.ts new file mode 100644 index 0000000..1bfa6a5 --- /dev/null +++ b/language-server/src/features/references.test.ts @@ -0,0 +1,203 @@ +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { ReferencesRequest } from "vscode-languageserver"; +import { TestClient } from "../test-client.js"; +import documentSettings from "./document-settings.js"; +import schemaRegistry from "./schema-registry.js"; +import workspace from "./workspace.js"; +import ReferencesFeature from "./references.js"; + +import type { DocumentSettings } from "./document-settings.js"; + + +describe("Feature - References", () => { + let client: TestClient; + + beforeEach(async () => { + client = new TestClient([ + workspace, + documentSettings, + schemaRegistry, + ReferencesFeature + ]); + await client.start(); + }); + + afterEach(async () => { + await client.stop(); + }); + + test("no references", async () => { + const documentUri = await client.openDocument("./subject.schema.json", `{}`); + + const response = await client.sendRequest(ReferencesRequest.type, { + textDocument: { uri: documentUri }, + position: { + line: 0, + character: 1 + }, + context: { includeDeclaration: false } + }); + + expect(response).to.eql([]); + }); + + test("don't return references that do not match location", async () => { + const documentUri = await client.openDocument("./subject.schema.json", `{ + "$schema":"https://json-schema.org/draft/2020-12/schema", + "$ref": "#/definitions/locations", + "definitions":{ + "names": { + + }, + "locations": { + + } + }, +}`); + + const response = await client.sendRequest(ReferencesRequest.type, { + textDocument: { uri: documentUri }, + position: { + line: 5, + character: 4 + }, + context: { includeDeclaration: false } + }); + + expect(response).to.eql([]); + }); + + + test("match one reference", async () => { + const documentUri = await client.openDocument("./subject.schema.json", `{ + "$schema":"https://json-schema.org/draft/2020-12/schema", + "$ref": "#/$defs/names", + "$defs":{ + "names": { + + } + }, +}`); + + const response = await client.sendRequest(ReferencesRequest.type, { + textDocument: { uri: documentUri }, + position: { + line: 5, + character: 4 + }, + context: { includeDeclaration: false } + }); + + expect(response).to.eql([ + { + "uri": documentUri, + "range": { + "start": { "line": 2, "character": 10 }, + "end": { "line": 2, "character": 25 } + } + } + ]); + }); + + test("cross file reference", async () => { + const documentUriA = await client.openDocument("./subjectA.schema.json", `{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "person": { + + } + } +} +`); + const documentUriB = await client.openDocument("./subjectB.schema.json", `{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "./subjectA.schema.json#/definitions/person" +} +`); + + const response = await client.sendRequest(ReferencesRequest.type, { + textDocument: { uri: documentUriA }, + position: { + line: 4, + character: 4 + }, + context: { includeDeclaration: false } + }); + + expect(response).to.eql([ + { + "uri": documentUriB, + "range": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 3, "character": 1 } + } + } + ]); + }); + + test("match self identified externally", async () => { + const documentUri = await client.openDocument("./subject.schema.json", `{ + "$schema":"http://json-schema.org/draft-07/schema#", + "$ref": "https://example.com/schemas/two#/definitions/names", +}`); + + const documentUriB = await client.openDocument("./subjectB.schema.json", `{ + "$schema":"http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/schemas/two", + "definitions":{ + "names": { + + } + } +}`); + + const response = await client.sendRequest(ReferencesRequest.type, { + textDocument: { uri: documentUriB }, + position: { + line: 5, + character: 4 + }, + context: { includeDeclaration: false } + }); + + expect(response).to.eql([ + { + "uri": documentUri, + "range": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 3, "character": 1 } + } + } + ]); + }); + + test("match self identified internally", async () => { + const documentUri = await client.openDocument("./subject.schema.json", `{ + "$schema":"http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/person.json", + "type": "object", + "properties": { + "names": { "$ref": "https://example.com/person.json" } + } +}`); + + const response = await client.sendRequest(ReferencesRequest.type, { + textDocument: { uri: documentUri }, + position: { + line: 2, + character: 46 + }, + context: { includeDeclaration: false } + }); + + expect(response).to.eql([ + { + "uri": documentUri, + "range": { + "start": { "line": 5, "character": 13 }, + "end": { "line": 5, "character": 58 } + } + } + ]); + }); +}); diff --git a/language-server/src/features/schema-completion.test.ts b/language-server/src/features/schema-completion.test.ts index a7eb973..902986e 100644 --- a/language-server/src/features/schema-completion.test.ts +++ b/language-server/src/features/schema-completion.test.ts @@ -2,6 +2,7 @@ import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest"; import { CompletionItemKind, CompletionRequest } from "vscode-languageserver"; import { TestClient } from "../test-client.js"; import completion from "./completion.js"; +import schemaRegistry from "./schema-registry.js"; import schemaCompletion from "./schema-completion.js"; import type { DocumentSettings } from "./document-settings.js"; @@ -12,7 +13,7 @@ describe("Feature - $schema completion", () => { let documentUri: string; beforeAll(async () => { - client = new TestClient([completion, schemaCompletion]); + client = new TestClient([completion, schemaCompletion, schemaRegistry]); await client.start(); }); diff --git a/language-server/src/features/semantic-tokens.test.ts b/language-server/src/features/semantic-tokens.test.ts index 79e04d8..79da7fb 100644 --- a/language-server/src/features/semantic-tokens.test.ts +++ b/language-server/src/features/semantic-tokens.test.ts @@ -1,10 +1,11 @@ -import { beforeAll, afterAll, describe, expect, test } from "vitest"; +import { beforeAll, afterAll, afterEach, describe, expect, test } from "vitest"; import { SemanticTokensRequest } from "vscode-languageserver"; import { TestClient } from "../test-client.js"; import semanticTokensFeature from "./semantic-tokens.js"; import workspace from "./workspace.js"; import type { DocumentSettings } from "./document-settings.js"; import documentSettings from "./document-settings.js"; +import schemaRegistry from "./schema-registry.js"; describe("Feature - Semantic Tokens", () => { @@ -14,6 +15,7 @@ describe("Feature - Semantic Tokens", () => { beforeAll(async () => { client = new TestClient([ documentSettings, + schemaRegistry, semanticTokensFeature, workspace ]); @@ -21,6 +23,10 @@ describe("Feature - Semantic Tokens", () => { await client.start(); }); + afterEach(async () => { + await client.closeDocument(documentUri); + }); + afterAll(async () => { await client.stop(); }); diff --git a/language-server/src/features/validate-references.js b/language-server/src/features/validate-references.js index 3af2c27..5354803 100644 --- a/language-server/src/features/validate-references.js +++ b/language-server/src/features/validate-references.js @@ -40,7 +40,7 @@ export default { }; /** @type Type.references */ -const references = function* (schemaResource) { +export const references = function* (schemaResource) { const refToken = keywordNameFor("https://json-schema.org/keyword/ref", schemaResource.dialectUri); const legacyRefToken = keywordNameFor("https://json-schema.org/keyword/draft-04/ref", schemaResource.dialectUri); diff --git a/language-server/src/server.js b/language-server/src/server.js index 1e20ce3..f09473a 100644 --- a/language-server/src/server.js +++ b/language-server/src/server.js @@ -15,6 +15,7 @@ import completionFeature from "./features/completion.js"; import ifThenCompletionFeature from "./features/if-then-completion.js"; import schemaCompletion from "./features/schema-completion.js"; import hoverFeature from "./features/hover.js"; +import referencesFeature from "./features/references.js"; const features = [ @@ -28,6 +29,7 @@ const features = [ schemaCompletion, ifThenCompletionFeature, hoverFeature, + referencesFeature, workspaceFeature ]; diff --git a/language-server/src/test-client.ts b/language-server/src/test-client.ts index a732eba..ef9d132 100644 --- a/language-server/src/test-client.ts +++ b/language-server/src/test-client.ts @@ -40,6 +40,7 @@ export class TestClient { private _settings: Partial | undefined; private _configurationChangeNotificationOptions: DidChangeConfigurationRegistrationOptions | null | undefined; private openDocuments: Set = new Set(); + private baseUri: string; onRequest: Connection["onRequest"]; sendRequest: Connection["sendRequest"]; @@ -50,6 +51,7 @@ export class TestClient { constructor(features: Feature[], serverName: string = "jsonSchemaLanguageServer") { this._serverName = serverName; + this.baseUri = pathToFileURL(`/${randomUUID()}/`).toString(); const up = new TestStream(); const down = new TestStream(); @@ -218,8 +220,7 @@ export class TestClient { } async openDocument(uri: string, text?: string) { - const baseUri = pathToFileURL(`/${randomUUID()}/`).toString(); - const documentUri = resolveIri(uri, baseUri); + const documentUri = resolveIri(uri, this.baseUri); await this._client.sendNotification(DidOpenTextDocumentNotification.type, { textDocument: {