Skip to content
Merged
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
5 changes: 4 additions & 1 deletion src/jsonLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ import {
FoldingRange, JSONSchema, SelectionRange, FoldingRangesContext, DocumentSymbolsContext, ColorInformationContext as DocumentColorsContext,
TextDocument,
Position, CompletionItem, CompletionList, Hover, Range, SymbolInformation, Diagnostic,
TextEdit, FormattingOptions, DocumentSymbol
TextEdit, FormattingOptions, DocumentSymbol, DefinitionLink
} from './jsonLanguageTypes';
import { findDefinition } from './services/jsonDefinition';

export type JSONDocument = {};
export * from './jsonLanguageTypes';
Expand All @@ -47,6 +48,7 @@ export interface LanguageService {
format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[];
getFoldingRanges(document: TextDocument, context?: FoldingRangesContext): FoldingRange[];
getSelectionRanges(document: TextDocument, positions: Position[], doc: JSONDocument): SelectionRange[];
findDefinition(document: TextDocument, position: Position, doc: JSONDocument): Thenable<DefinitionLink[]>;
}


Expand Down Expand Up @@ -85,6 +87,7 @@ export function getLanguageService(params: LanguageServiceParams): LanguageServi
doHover: jsonHover.doHover.bind(jsonHover),
getFoldingRanges,
getSelectionRanges,
findDefinition,
format: (d, r, o) => {
let range: JSONCRange | undefined = undefined;
if (r) {
Expand Down
96 changes: 96 additions & 0 deletions src/services/jsonDefinition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { JSONSchemaRef, JSONSchema } from '../jsonSchema';
import { DefinitionLink, Position, TextDocument, ASTNode, PropertyASTNode, Range, Thenable } from '../jsonLanguageTypes';
import { JSONDocument } from '../parser/jsonParser';

export function findDefinition(document: TextDocument, position: Position, doc: JSONDocument): Thenable<DefinitionLink[]> {
const offset = document.offsetAt(position);
const node = doc.getNodeFromOffset(offset, true);
if (!node || !isRef(node)) {
return Promise.resolve([]);
}

const propertyNode: PropertyASTNode = node.parent as PropertyASTNode;
const valueNode = propertyNode.valueNode as ASTNode;
const path = valueNode.value as string;
const targetNode = findTargetNode(doc, path);
if (!targetNode) {
return Promise.resolve([]);
}
const definition: DefinitionLink = {
targetUri: document.uri,
originSelectionRange: createRange(document, valueNode),
targetRange: createRange(document, targetNode),
targetSelectionRange: createRange(document, targetNode)
};
return Promise.resolve([definition]);
}

function createRange(document: TextDocument, node: ASTNode): Range {
return Range.create(document.positionAt(node.offset), document.positionAt(node.offset + node.length));
}

function isRef(node: ASTNode): boolean {
return node.type === 'string' &&
node.parent &&
node.parent.type === 'property' &&
node.parent.valueNode === node &&
node.parent.keyNode.value === "$ref" ||
false;
}

function findTargetNode(doc: JSONDocument, path: string): ASTNode | null {
const tokens = parseJSONPointer(path);
if (!tokens) {
return null;
}
return findNode(tokens, doc.root);
}

function findNode(pointer: string[], node: ASTNode | null | undefined): ASTNode | null {
if (!node) {
return null;
}
if (pointer.length === 0) {
return node;
}

const token: string = pointer.shift() as string;
if (node && node.type === 'object') {
const propertyNode: PropertyASTNode | undefined = node.properties.find((propertyNode) => propertyNode.keyNode.value === token);
if (!propertyNode) {
return null;
}
return findNode(pointer, propertyNode.valueNode);
} else if (node && node.type === 'array') {
if (token.match(/^(0|[1-9][0-9]*)$/)) {
const index = Number.parseInt(token);
const arrayItem = node.items[index];
if (!arrayItem) {
return null;
}
return findNode(pointer, arrayItem);
}
}
return null;
}

function parseJSONPointer(path: string): string[] | null {
if (path === "#") {
return [];
}

if (path[0] !== '#' || path[1] !== '/') {
return null;
}

return path.substring(2).split(/\//).map(unescape);
}

function unescape(str: string): string {
return str.replace(/~1/g, '/').replace(/~0/g, '~');
}
66 changes: 66 additions & 0 deletions src/test/definition.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as assert from 'assert';

import { getLanguageService, JSONSchema, TextDocument, ClientCapabilities, CompletionList, CompletionItemKind, Position, MarkupContent } from '../jsonLanguageService';
import { repeat } from '../utils/strings';
import { DefinitionLink } from 'vscode-languageserver-types';

suite('JSON Find Definitions', () => {
const testFindDefinitionFor = function (value: string, expected: {offset: number, length: number} | null): PromiseLike<void> {
const offset = value.indexOf('|');
value = value.substr(0, offset) + value.substr(offset + 1);

const ls = getLanguageService({ clientCapabilities: ClientCapabilities.LATEST });
const document = TextDocument.create('test://test/test.json', 'json', 0, value);
const position = Position.create(0, offset);
const jsonDoc = ls.parseJSONDocument(document);
return ls.findDefinition(document, position, jsonDoc).then(list => {
if (expected) {
assert.notDeepEqual(list, []);
const startOffset = list[0].targetRange.start.character;
assert.equal(startOffset, expected.offset);
assert.equal(list[0].targetRange.end.character - startOffset, expected.length);
} else {
assert.deepEqual(list, []);
}
});
};

test('FindDefinition invalid ref', async function () {
await testFindDefinitionFor('{|}', null);
await testFindDefinitionFor('{"name": |"John"}', null);
await testFindDefinitionFor('{"|name": "John"}', null);
await testFindDefinitionFor('{"name": "|John"}', null);
await testFindDefinitionFor('{"name": "John", "$ref": "#/|john/name"}', null);
await testFindDefinitionFor('{"name": "John", "$ref|": "#/name"}', null);
await testFindDefinitionFor('{"name": "John", "$ref": "#/|"}', null);
});

test('FindDefinition valid ref', async function () {
await testFindDefinitionFor('{"name": "John", "$ref": "#/n|ame"}', {offset: 9, length: 6});
await testFindDefinitionFor('{"name": "John", "$ref": "|#/name"}', {offset: 9, length: 6});
await testFindDefinitionFor('{"name": "John", "$ref": |"#/name"}', {offset: 9, length: 6});
await testFindDefinitionFor('{"name": "John", "$ref": "#/name"|}', {offset: 9, length: 6});
await testFindDefinitionFor('{"name": "John", "$ref": "#/name|"}', {offset: 9, length: 6});
await testFindDefinitionFor('{"name": "John", "$ref": "#|"}', {offset: 0, length: 29});

const doc = (ref: string) => `{"foo": ["bar", "baz"],"": 0,"a/b": 1,"c%d": 2,"e^f": 3,"i\\\\j": 5,"k\\"l": 6," ": 7,"m~n": 8, "$ref": "|${ref}"}`;
await testFindDefinitionFor(doc('#'), {offset: 0, length: 105});
await testFindDefinitionFor(doc('#/foo'), {offset: 8, length: 14});
await testFindDefinitionFor(doc('#/foo/0'), {offset: 9, length: 5});
await testFindDefinitionFor(doc('#/foo/1'), {offset: 16, length: 5});
await testFindDefinitionFor(doc('#/foo/01'), null);
await testFindDefinitionFor(doc('#/'), {offset: 27, length: 1});
await testFindDefinitionFor(doc('#/a~1b'), {offset: 36, length: 1});
await testFindDefinitionFor(doc('#/c%d'), {offset: 45, length: 1});
await testFindDefinitionFor(doc('#/e^f'), {offset: 54, length: 1});
await testFindDefinitionFor(doc('#/i\\\\j'), {offset: 64, length: 1});
await testFindDefinitionFor(doc('#/k\\"l'), {offset: 74, length: 1});
await testFindDefinitionFor(doc('#/ '), {offset: 81, length: 1});
await testFindDefinitionFor(doc('#/m~0n'), {offset: 90, length: 1});
});
});