Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert "hover" action to return MarkedContent instead of MarkedString #77

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
6 changes: 3 additions & 3 deletions src/jsonContributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<MarkedString[]>;
getInfoContribution(uri: string, location: JSONPath): Thenable<Hover["contents"]>;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a wider type, so is fully backwards compatible.

collectPropertyCompletions(uri: string, location: JSONPath, currentWord: string, addValue: boolean, isLast: boolean, result: CompletionsCollector): Thenable<any>;
collectValueCompletions(uri: string, location: JSONPath, propertyKey: string, result: CompletionsCollector): Thenable<any>;
collectDefaultCompletions(uri: string, result: CompletionsCollector): Thenable<any>;
Expand All @@ -20,4 +20,4 @@ export interface CompletionsCollector {
log(message: string): void;
setAsIncomplete(): void;
getNumberOfProposals(): number;
}
}
61 changes: 36 additions & 25 deletions src/services/jsonHover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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;
});
Expand All @@ -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;
}
}
90 changes: 73 additions & 17 deletions src/test/hover.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@ import * as JsonSchema from '../jsonSchema';
import { JSONHover } from '../services/jsonHover';

import { Hover, Position, MarkedString, TextDocument } from '../jsonLanguageService';
import { JSONWorkerContribution, MarkupKind } from "../jsonLanguageTypes";

suite('JSON Hover', () => {

function testComputeInfo(value: string, schema: JsonSchema.JSONSchema, position: Position): PromiseLike<Hover> {
function testComputeInfo(
value: string,
schema: JsonSchema.JSONSchema,
position: Position,
contributions: JSONWorkerContribution[] = [],
): PromiseLike<Hover> {
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);

Expand Down Expand Up @@ -52,16 +58,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.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, [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, [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, [MarkedString.fromPlainText('A')]);
assert.deepStrictEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('A') });
});
});

Expand All @@ -88,13 +94,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.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, [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, [MarkedString.fromPlainText('B\n\nIt\'s B')]);
assert.deepStrictEqual(result.contents, { kind: MarkupKind.PlainText, value: MarkedString.fromPlainText('B\n\nIt\'s B') });
});
});

Expand Down Expand Up @@ -123,19 +129,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.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, ['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, ['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, ['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, ['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`' });
});
});

Expand All @@ -153,10 +159,60 @@ 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.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, ['line1\n\nline2\r\n\r\nline3']);
assert.deepStrictEqual(result.contents, { kind: MarkupKind.PlainText, value: 'line1\r\nline2\r\n\r\nline3' });
});
});
});

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.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.deepStrictEqual(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.deepStrictEqual(result.contents, { kind: MarkupKind.PlainText, value: 'Custom contribution info' });
});
});
});