From 33577eb5bfac30fd36cf78aa5b9556ec0be4bec5 Mon Sep 17 00:00:00 2001 From: Cameron Little Date: Thu, 1 Oct 2020 09:24:58 +0200 Subject: [PATCH 1/3] Update hover to not use deprecated MarkedString --- src/jsonContributions.ts | 6 ++-- src/services/jsonHover.ts | 61 +++++++++++++++++++++++---------------- src/test/hover.test.ts | 53 ++++++++++++++++++++++++---------- 3 files changed, 77 insertions(+), 43 deletions(-) diff --git a/src/jsonContributions.ts b/src/jsonContributions.ts index ef886df4..145a8dc5 100644 --- a/src/jsonContributions.ts +++ b/src/jsonContributions.ts @@ -2,10 +2,10 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Thenable, MarkedString, CompletionItem } from './jsonLanguageService'; +import { Thenable, CompletionItem, Hover } from './jsonLanguageService'; export interface JSONWorkerContribution { - getInfoContribution(uri: string, location: JSONPath): Thenable; + getInfoContribution(uri: string, location: JSONPath): Thenable; collectPropertyCompletions(uri: string, location: JSONPath, currentWord: string, addValue: boolean, isLast: boolean, result: CompletionsCollector): Thenable; collectValueCompletions(uri: string, location: JSONPath, propertyKey: string, result: CompletionsCollector): Thenable; collectDefaultCompletions(uri: string, result: CompletionsCollector): Thenable; @@ -20,4 +20,4 @@ export interface CompletionsCollector { log(message: string): void; setAsIncomplete(): void; getNumberOfProposals(): number; -} \ No newline at end of file +} diff --git a/src/services/jsonHover.ts b/src/services/jsonHover.ts index db2b1660..17db31a0 100644 --- a/src/services/jsonHover.ts +++ b/src/services/jsonHover.ts @@ -6,7 +6,7 @@ import * as Parser from '../parser/jsonParser'; import * as SchemaService from './jsonSchemaService'; import { JSONWorkerContribution } from '../jsonContributions'; -import { TextDocument, PromiseConstructor, Thenable, Position, Range, Hover, MarkedString } from '../jsonLanguageTypes'; +import { TextDocument, PromiseConstructor, Thenable, Position, Range, Hover, MarkupContent, MarkupKind } from '../jsonLanguageTypes'; export class JSONHover { @@ -42,9 +42,9 @@ export class JSONHover { const hoverRange = Range.create(document.positionAt(hoverRangeNode.offset), document.positionAt(hoverRangeNode.offset + hoverRangeNode.length)); - var createHover = (contents: MarkedString[]) => { + var createHover = (contents: Hover["contents"]) => { const result: Hover = { - contents: contents, + contents, range: hoverRange }; return result; @@ -54,30 +54,38 @@ export class JSONHover { for (let i = this.contributions.length - 1; i >= 0; i--) { const contribution = this.contributions[i]; const promise = contribution.getInfoContribution(document.uri, location); - if (promise) { - return promise.then(htmlContent => createHover(htmlContent)); - } + return promise?.then(htmlContent => createHover(htmlContent)); } return this.schemaService.getSchemaForResource(document.uri, doc).then((schema) => { if (schema && node) { const matchingSchemas = doc.getMatchingSchemas(schema.schema, node.offset); + let markdownFormat: boolean = false; let title: string | undefined = undefined; - let markdownDescription: string | undefined = undefined; - let markdownEnumValueDescription: string | undefined = undefined, enumValue: string | undefined = undefined; + let description: string | undefined = undefined; + let enumValueDescription: string | undefined = undefined, enumValue: string | undefined = undefined; matchingSchemas.every((s) => { if (s.node === node && !s.inverted && s.schema) { title = title || s.schema.title; - markdownDescription = markdownDescription || s.schema.markdownDescription || toMarkdown(s.schema.description); + if (!description) { + if (s.schema.markdownDescription) { + markdownFormat = true; + description = s.schema.markdownDescription; + } else { + description = s.schema.description; + } + } if (s.schema.enum) { const idx = s.schema.enum.indexOf(Parser.getNodeValue(node)); if (s.schema.markdownEnumDescriptions) { - markdownEnumValueDescription = s.schema.markdownEnumDescriptions[idx]; + enumValueDescription = s.schema.markdownEnumDescriptions[idx]; } else if (s.schema.enumDescriptions) { - markdownEnumValueDescription = toMarkdown(s.schema.enumDescriptions[idx]); + enumValueDescription = s.schema.enumDescriptions[idx]; } - if (markdownEnumValueDescription) { + if (enumValueDescription) { + // enums values are always wrapped as code blocks, so they'll always be presented as markdown + markdownFormat = true; enumValue = s.schema.enum[idx]; if (typeof enumValue !== 'string') { enumValue = JSON.stringify(enumValue); @@ -87,23 +95,26 @@ export class JSONHover { } return true; }); - let result = ''; + const result: MarkupContent = { + kind: markdownFormat ? MarkupKind.Markdown : MarkupKind.PlainText, + value: '', + }; if (title) { - result = toMarkdown(title); + result.value += markdownFormat ? toMarkdown(title) : title; } - if (markdownDescription) { - if (result.length > 0) { - result += "\n\n"; + if (description) { + if (result.value.length > 0) { + result.value += "\n\n"; } - result += markdownDescription; + result.value += description; } - if (markdownEnumValueDescription) { - if (result.length > 0) { - result += "\n\n"; + if (enumValueDescription) { + if (result.value.length > 0) { + result.value += "\n\n"; } - result += `\`${toMarkdownCodeBlock(enumValue!)}\`: ${markdownEnumValueDescription}`; + result.value += `\`${toMarkdownCodeBlock(enumValue!)}\`: ${enumValueDescription}`; } - return createHover([result]); + return createHover(result); } return null; }); @@ -121,8 +132,8 @@ function toMarkdown(plain: string | undefined): string | undefined { function toMarkdownCodeBlock(content: string) { // see https://daringfireball.net/projects/markdown/syntax#precode - if (content.indexOf('`') !== -1) { + if (content.includes('`')) { return '`` ' + content + ' ``'; } return content; -} \ No newline at end of file +} diff --git a/src/test/hover.test.ts b/src/test/hover.test.ts index 52728eaf..71bff841 100644 --- a/src/test/hover.test.ts +++ b/src/test/hover.test.ts @@ -10,6 +10,7 @@ import * as JsonSchema from '../jsonSchema'; import { JSONHover } from '../services/jsonHover'; import { Hover, Position, MarkedString, TextDocument } from '../jsonLanguageService'; +import { MarkupKind } from "../jsonLanguageTypes"; suite('JSON Hover', () => { @@ -52,16 +53,16 @@ suite('JSON Hover', () => { } }; await testComputeInfo(content, schema, { line: 0, character: 0 }).then((result) => { - assert.deepEqual(result.contents, [MarkedString.fromPlainText('a very special object')]); + assert.deepEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('a very special object') }); }); await testComputeInfo(content, schema, { line: 0, character: 1 }).then((result) => { - assert.deepEqual(result.contents, [MarkedString.fromPlainText('A')]); + assert.deepEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('A') }); }); await testComputeInfo(content, schema, { line: 0, character: 32 }).then((result) => { - assert.deepEqual(result.contents, [MarkedString.fromPlainText('C')]); + assert.deepEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('C') }); }); await testComputeInfo(content, schema, { line: 0, character: 7 }).then((result) => { - assert.deepEqual(result.contents, [MarkedString.fromPlainText('A')]); + assert.deepEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('A') }); }); }); @@ -88,13 +89,13 @@ suite('JSON Hover', () => { }] }; await testComputeInfo(content, schema, { line: 0, character: 0 }).then((result) => { - assert.deepEqual(result.contents, [MarkedString.fromPlainText('a very special object')]); + assert.deepEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('a very special object') }); }); await testComputeInfo(content, schema, { line: 0, character: 1 }).then((result) => { - assert.deepEqual(result.contents, [MarkedString.fromPlainText('A')]); + assert.deepEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('A') }); }); await testComputeInfo(content, schema, { line: 0, character: 10 }).then((result) => { - assert.deepEqual(result.contents, [MarkedString.fromPlainText('B\n\nIt\'s B')]); + assert.deepEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('B\n\nIt\'s B') }); }); }); @@ -123,19 +124,19 @@ suite('JSON Hover', () => { }; await testComputeInfo('{ "prop1": "e1', schema, { line: 0, character: 12 }).then(result => { - assert.deepEqual(result.contents, ['prop1\n\n`e1`: E1']); + assert.deepEqual(result.contents, { kind: MarkupKind.Markdown, value: 'prop1\n\n`e1`: E1' }); }); await testComputeInfo('{ "prop2": null', schema, { line: 0, character: 12 }).then(result => { - assert.deepEqual(result.contents, ['prop2\n\n`null`: null']); + assert.deepEqual(result.contents, { kind: MarkupKind.Markdown, value: 'prop2\n\n`null`: null' }); }); await testComputeInfo('{ "prop2": 1', schema, { line: 0, character: 11 }).then(result => { - assert.deepEqual(result.contents, ['prop2\n\n`1`: one']); + assert.deepEqual(result.contents, { kind: MarkupKind.Markdown, value: 'prop2\n\n`1`: one' }); }); await testComputeInfo('{ "prop2": false', schema, { line: 0, character: 12 }).then(result => { - assert.deepEqual(result.contents, ['prop2\n\n`false`: wrong']); + assert.deepEqual(result.contents, { kind: MarkupKind.Markdown, value: 'prop2\n\n`false`: wrong' }); }); await testComputeInfo('{ "prop3": null', schema, { line: 0, character: 12 }).then(result => { - assert.deepEqual(result.contents, ['title\n\n*prop3*\n\n`null`: Set to `null`']); + assert.deepEqual(result.contents, { kind: MarkupKind.Markdown, value: 'title\n\n*prop3*\n\n`null`: Set to `null`' }); }); }); @@ -153,10 +154,32 @@ suite('JSON Hover', () => { }; await testComputeInfo('{ "prop1": "e1', schema, { line: 0, character: 12 }).then(result => { - assert.deepEqual(result.contents, ['line1\n\nline2\n\nline3\n\n\nline4\n']); + assert.deepEqual(result.contents, { kind: MarkupKind.PlainText, value: 'line1\nline2\n\nline3\n\n\nline4\n' }); }); await testComputeInfo('{ "prop2": "e1', schema, { line: 0, character: 12 }).then(result => { - assert.deepEqual(result.contents, ['line1\n\nline2\r\n\r\nline3']); + assert.deepEqual(result.contents, { kind: MarkupKind.PlainText, value: 'line1\r\nline2\r\n\r\nline3' }); }); }); -}); \ No newline at end of file + + test('Markdown descriptions', async function () { + const schema: JsonSchema.JSONSchema = { + type: 'object', + properties: { + 'prop1': { + markdownDescription: "line1\nline2\n\n`line3`\n\n\nline4\n", + }, + 'prop2': { + title: `Title with *markdown* characters`, + markdownDescription: "line1\r\n*line2*\r\n\r\n`line3`", + } + } + }; + + await testComputeInfo('{ "prop1": "e1', schema, { line: 0, character: 12 }).then(result => { + assert.deepEqual(result.contents, { kind: MarkupKind.Markdown, value: 'line1\nline2\n\n`line3`\n\n\nline4\n' }); + }); + await testComputeInfo('{ "prop2": "e1', schema, { line: 0, character: 12 }).then(result => { + assert.deepEqual(result.contents, { kind: MarkupKind.Markdown, value: 'Title with \\*markdown\\* characters\n\nline1\r\n*line2*\r\n\r\n`line3`' }); + }); + }); +}); From 89eb0dd74de6b109c73641ee9f6958c4b50d6ccf Mon Sep 17 00:00:00 2001 From: Cameron Little Date: Thu, 1 Oct 2020 09:25:34 +0200 Subject: [PATCH 2/3] Add test for hover contributions Fixes #53 --- src/test/hover.test.ts | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/test/hover.test.ts b/src/test/hover.test.ts index 71bff841..6ef954db 100644 --- a/src/test/hover.test.ts +++ b/src/test/hover.test.ts @@ -10,15 +10,20 @@ import * as JsonSchema from '../jsonSchema'; import { JSONHover } from '../services/jsonHover'; import { Hover, Position, MarkedString, TextDocument } from '../jsonLanguageService'; -import { MarkupKind } from "../jsonLanguageTypes"; +import { JSONWorkerContribution, MarkupKind } from "../jsonLanguageTypes"; suite('JSON Hover', () => { - function testComputeInfo(value: string, schema: JsonSchema.JSONSchema, position: Position): PromiseLike { + function testComputeInfo( + value: string, + schema: JsonSchema.JSONSchema, + position: Position, + contributions: JSONWorkerContribution[] = [], + ): PromiseLike { const uri = 'test://test.json'; const schemaService = new SchemaService.JSONSchemaService(requestService); - const hoverProvider = new JSONHover(schemaService, [], Promise); + const hoverProvider = new JSONHover(schemaService, contributions, Promise); const id = "http://myschemastore/test1"; schemaService.registerExternalSchema(id, ["*.json"], schema); @@ -182,4 +187,32 @@ suite('JSON Hover', () => { assert.deepEqual(result.contents, { kind: MarkupKind.Markdown, value: 'Title with \\*markdown\\* characters\n\nline1\r\n*line2*\r\n\r\n`line3`' }); }); }); + + test("Hover contributions", async () => { + const content = '{"a": 42, "b": "hello", "c": false}'; + const schema: JsonSchema.JSONSchema = {}; + const contribution: JSONWorkerContribution = { + async getInfoContribution(uri, location) { + return { + kind: MarkupKind.PlainText, + value: "Custom contribution info" + }; + }, + async collectPropertyCompletions(uri, location, currentWord, addValue, isLast, result) { + assert.fail(); + }, + async collectValueCompletions(uri, location, propertyKey, result) { + assert.fail(); + }, + async collectDefaultCompletions(uri, result) { + assert.fail(); + }, + async resolveCompletion(item) { + assert.fail(); + } + }; + await testComputeInfo(content, schema, { line: 0, character: 7 }, [contribution]).then((result) => { + assert.deepEqual(result.contents, { kind: MarkupKind.PlainText, value: 'Custom contribution info' }); + }); + }); }); From a4f3de97e7f3c99a60a9da061054d3d38bace715 Mon Sep 17 00:00:00 2001 From: Cameron Little Date: Thu, 1 Oct 2020 09:26:24 +0200 Subject: [PATCH 3/3] Replace deprecated test method in hover tests --- src/test/hover.test.ts | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/test/hover.test.ts b/src/test/hover.test.ts index 6ef954db..3665ab36 100644 --- a/src/test/hover.test.ts +++ b/src/test/hover.test.ts @@ -58,16 +58,16 @@ suite('JSON Hover', () => { } }; await testComputeInfo(content, schema, { line: 0, character: 0 }).then((result) => { - assert.deepEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('a very special object') }); + assert.deepStrictEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('a very special object') }); }); await testComputeInfo(content, schema, { line: 0, character: 1 }).then((result) => { - assert.deepEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('A') }); + assert.deepStrictEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('A') }); }); await testComputeInfo(content, schema, { line: 0, character: 32 }).then((result) => { - assert.deepEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('C') }); + assert.deepStrictEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('C') }); }); await testComputeInfo(content, schema, { line: 0, character: 7 }).then((result) => { - assert.deepEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('A') }); + assert.deepStrictEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('A') }); }); }); @@ -94,13 +94,13 @@ suite('JSON Hover', () => { }] }; await testComputeInfo(content, schema, { line: 0, character: 0 }).then((result) => { - assert.deepEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('a very special object') }); + assert.deepStrictEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('a very special object') }); }); await testComputeInfo(content, schema, { line: 0, character: 1 }).then((result) => { - assert.deepEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('A') }); + assert.deepStrictEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('A') }); }); await testComputeInfo(content, schema, { line: 0, character: 10 }).then((result) => { - assert.deepEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('B\n\nIt\'s B') }); + assert.deepStrictEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('B\n\nIt\'s B') }); }); }); @@ -129,19 +129,19 @@ suite('JSON Hover', () => { }; await testComputeInfo('{ "prop1": "e1', schema, { line: 0, character: 12 }).then(result => { - assert.deepEqual(result.contents, { kind: MarkupKind.Markdown, value: 'prop1\n\n`e1`: E1' }); + assert.deepStrictEqual(result.contents, { kind: MarkupKind.Markdown, value: 'prop1\n\n`e1`: E1' }); }); await testComputeInfo('{ "prop2": null', schema, { line: 0, character: 12 }).then(result => { - assert.deepEqual(result.contents, { kind: MarkupKind.Markdown, value: 'prop2\n\n`null`: null' }); + assert.deepStrictEqual(result.contents, { kind: MarkupKind.Markdown, value: 'prop2\n\n`null`: null' }); }); await testComputeInfo('{ "prop2": 1', schema, { line: 0, character: 11 }).then(result => { - assert.deepEqual(result.contents, { kind: MarkupKind.Markdown, value: 'prop2\n\n`1`: one' }); + assert.deepStrictEqual(result.contents, { kind: MarkupKind.Markdown, value: 'prop2\n\n`1`: one' }); }); await testComputeInfo('{ "prop2": false', schema, { line: 0, character: 12 }).then(result => { - assert.deepEqual(result.contents, { kind: MarkupKind.Markdown, value: 'prop2\n\n`false`: wrong' }); + assert.deepStrictEqual(result.contents, { kind: MarkupKind.Markdown, value: 'prop2\n\n`false`: wrong' }); }); await testComputeInfo('{ "prop3": null', schema, { line: 0, character: 12 }).then(result => { - assert.deepEqual(result.contents, { kind: MarkupKind.Markdown, value: 'title\n\n*prop3*\n\n`null`: Set to `null`' }); + assert.deepStrictEqual(result.contents, { kind: MarkupKind.Markdown, value: 'title\n\n*prop3*\n\n`null`: Set to `null`' }); }); }); @@ -159,10 +159,10 @@ suite('JSON Hover', () => { }; await testComputeInfo('{ "prop1": "e1', schema, { line: 0, character: 12 }).then(result => { - assert.deepEqual(result.contents, { kind: MarkupKind.PlainText, value: 'line1\nline2\n\nline3\n\n\nline4\n' }); + assert.deepStrictEqual(result.contents, { kind: MarkupKind.PlainText, value: 'line1\nline2\n\nline3\n\n\nline4\n' }); }); await testComputeInfo('{ "prop2": "e1', schema, { line: 0, character: 12 }).then(result => { - assert.deepEqual(result.contents, { kind: MarkupKind.PlainText, value: 'line1\r\nline2\r\n\r\nline3' }); + assert.deepStrictEqual(result.contents, { kind: MarkupKind.PlainText, value: 'line1\r\nline2\r\n\r\nline3' }); }); }); @@ -181,10 +181,10 @@ suite('JSON Hover', () => { }; await testComputeInfo('{ "prop1": "e1', schema, { line: 0, character: 12 }).then(result => { - assert.deepEqual(result.contents, { kind: MarkupKind.Markdown, value: 'line1\nline2\n\n`line3`\n\n\nline4\n' }); + assert.deepStrictEqual(result.contents, { kind: MarkupKind.Markdown, value: 'line1\nline2\n\n`line3`\n\n\nline4\n' }); }); await testComputeInfo('{ "prop2": "e1', schema, { line: 0, character: 12 }).then(result => { - assert.deepEqual(result.contents, { kind: MarkupKind.Markdown, value: 'Title with \\*markdown\\* characters\n\nline1\r\n*line2*\r\n\r\n`line3`' }); + assert.deepStrictEqual(result.contents, { kind: MarkupKind.Markdown, value: 'Title with \\*markdown\\* characters\n\nline1\r\n*line2*\r\n\r\n`line3`' }); }); }); @@ -212,7 +212,7 @@ suite('JSON Hover', () => { } }; await testComputeInfo(content, schema, { line: 0, character: 7 }, [contribution]).then((result) => { - assert.deepEqual(result.contents, { kind: MarkupKind.PlainText, value: 'Custom contribution info' }); + assert.deepStrictEqual(result.contents, { kind: MarkupKind.PlainText, value: 'Custom contribution info' }); }); }); });