Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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): 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 } from '../jsonLanguageTypes';
import { JSONDocument } from '../parser/jsonParser';

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

let propertyNode: PropertyASTNode = node.parent as PropertyASTNode;
let valueNode = propertyNode.valueNode as ASTNode;
let path = valueNode.value as string;
let targetNode = findTargetNode(doc, path);
if (!targetNode) {
return [];
}
let definition: DefinitionLink = {
targetUri: document.uri,
originSelectionRange: createRange(document, valueNode),
targetRange: createRange(document, targetNode),
targetSelectionRange: createRange(document, targetNode)
};
return [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 {
let 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') {
let 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);
let 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): 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);
const list = ls.findDefinition(document, position, jsonDoc);
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', function () {
testFindDefinitionFor('{|}', null);
testFindDefinitionFor('{"name": |"John"}', null);
testFindDefinitionFor('{"|name": "John"}', null);
testFindDefinitionFor('{"name": "|John"}', null);
testFindDefinitionFor('{"name": "John", "$ref": "#/|john/name"}', null);
testFindDefinitionFor('{"name": "John", "$ref|": "#/name"}', null);
testFindDefinitionFor('{"name": "John", "$ref": "#/|"}', null);
});

test('FindDefinition valid ref', function () {
testFindDefinitionFor('{"name": "John", "$ref": "#/n|ame"}', {offset: 9, length: 6});
testFindDefinitionFor('{"name": "John", "$ref": "|#/name"}', {offset: 9, length: 6});
testFindDefinitionFor('{"name": "John", "$ref": |"#/name"}', {offset: 9, length: 6});
testFindDefinitionFor('{"name": "John", "$ref": "#/name"|}', {offset: 9, length: 6});
testFindDefinitionFor('{"name": "John", "$ref": "#/name|"}', {offset: 9, length: 6});
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}"}`;
testFindDefinitionFor(doc('#'), {offset: 0, length: 105});
testFindDefinitionFor(doc('#/foo'), {offset: 8, length: 14});
testFindDefinitionFor(doc('#/foo/0'), {offset: 9, length: 5});
testFindDefinitionFor(doc('#/foo/1'), {offset: 16, length: 5});
testFindDefinitionFor(doc('#/foo/01'), null);
testFindDefinitionFor(doc('#/'), {offset: 27, length: 1});
testFindDefinitionFor(doc('#/a~1b'), {offset: 36, length: 1});
testFindDefinitionFor(doc('#/c%d'), {offset: 45, length: 1});
testFindDefinitionFor(doc('#/e^f'), {offset: 54, length: 1});
testFindDefinitionFor(doc('#/i\\\\j'), {offset: 64, length: 1});
testFindDefinitionFor(doc('#/k\\"l'), {offset: 74, length: 1});
testFindDefinitionFor(doc('#/ '), {offset: 81, length: 1});
testFindDefinitionFor(doc('#/m~0n'), {offset: 90, length: 1});
});
});