Skip to content

Commit 1fadf89

Browse files
authored
Merge pull request #50 from ananthakumaran/definition
add support for textDocument/definition
2 parents 104d3af + 6bbffca commit 1fadf89

File tree

3 files changed

+166
-1
lines changed

3 files changed

+166
-1
lines changed

src/jsonLanguageService.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ import {
2323
FoldingRange, JSONSchema, SelectionRange, FoldingRangesContext, DocumentSymbolsContext, ColorInformationContext as DocumentColorsContext,
2424
TextDocument,
2525
Position, CompletionItem, CompletionList, Hover, Range, SymbolInformation, Diagnostic,
26-
TextEdit, FormattingOptions, DocumentSymbol
26+
TextEdit, FormattingOptions, DocumentSymbol, DefinitionLink
2727
} from './jsonLanguageTypes';
28+
import { findDefinition } from './services/jsonDefinition';
2829

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

5254

@@ -85,6 +87,7 @@ export function getLanguageService(params: LanguageServiceParams): LanguageServi
8587
doHover: jsonHover.doHover.bind(jsonHover),
8688
getFoldingRanges,
8789
getSelectionRanges,
90+
findDefinition,
8891
format: (d, r, o) => {
8992
let range: JSONCRange | undefined = undefined;
9093
if (r) {

src/services/jsonDefinition.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { JSONSchemaRef, JSONSchema } from '../jsonSchema';
7+
import { DefinitionLink, Position, TextDocument, ASTNode, PropertyASTNode, Range, Thenable } from '../jsonLanguageTypes';
8+
import { JSONDocument } from '../parser/jsonParser';
9+
10+
export function findDefinition(document: TextDocument, position: Position, doc: JSONDocument): Thenable<DefinitionLink[]> {
11+
const offset = document.offsetAt(position);
12+
const node = doc.getNodeFromOffset(offset, true);
13+
if (!node || !isRef(node)) {
14+
return Promise.resolve([]);
15+
}
16+
17+
const propertyNode: PropertyASTNode = node.parent as PropertyASTNode;
18+
const valueNode = propertyNode.valueNode as ASTNode;
19+
const path = valueNode.value as string;
20+
const targetNode = findTargetNode(doc, path);
21+
if (!targetNode) {
22+
return Promise.resolve([]);
23+
}
24+
const definition: DefinitionLink = {
25+
targetUri: document.uri,
26+
originSelectionRange: createRange(document, valueNode),
27+
targetRange: createRange(document, targetNode),
28+
targetSelectionRange: createRange(document, targetNode)
29+
};
30+
return Promise.resolve([definition]);
31+
}
32+
33+
function createRange(document: TextDocument, node: ASTNode): Range {
34+
return Range.create(document.positionAt(node.offset), document.positionAt(node.offset + node.length));
35+
}
36+
37+
function isRef(node: ASTNode): boolean {
38+
return node.type === 'string' &&
39+
node.parent &&
40+
node.parent.type === 'property' &&
41+
node.parent.valueNode === node &&
42+
node.parent.keyNode.value === "$ref" ||
43+
false;
44+
}
45+
46+
function findTargetNode(doc: JSONDocument, path: string): ASTNode | null {
47+
const tokens = parseJSONPointer(path);
48+
if (!tokens) {
49+
return null;
50+
}
51+
return findNode(tokens, doc.root);
52+
}
53+
54+
function findNode(pointer: string[], node: ASTNode | null | undefined): ASTNode | null {
55+
if (!node) {
56+
return null;
57+
}
58+
if (pointer.length === 0) {
59+
return node;
60+
}
61+
62+
const token: string = pointer.shift() as string;
63+
if (node && node.type === 'object') {
64+
const propertyNode: PropertyASTNode | undefined = node.properties.find((propertyNode) => propertyNode.keyNode.value === token);
65+
if (!propertyNode) {
66+
return null;
67+
}
68+
return findNode(pointer, propertyNode.valueNode);
69+
} else if (node && node.type === 'array') {
70+
if (token.match(/^(0|[1-9][0-9]*)$/)) {
71+
const index = Number.parseInt(token);
72+
const arrayItem = node.items[index];
73+
if (!arrayItem) {
74+
return null;
75+
}
76+
return findNode(pointer, arrayItem);
77+
}
78+
}
79+
return null;
80+
}
81+
82+
function parseJSONPointer(path: string): string[] | null {
83+
if (path === "#") {
84+
return [];
85+
}
86+
87+
if (path[0] !== '#' || path[1] !== '/') {
88+
return null;
89+
}
90+
91+
return path.substring(2).split(/\//).map(unescape);
92+
}
93+
94+
function unescape(str: string): string {
95+
return str.replace(/~1/g, '/').replace(/~0/g, '~');
96+
}

src/test/definition.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as assert from 'assert';
7+
8+
import { getLanguageService, JSONSchema, TextDocument, ClientCapabilities, CompletionList, CompletionItemKind, Position, MarkupContent } from '../jsonLanguageService';
9+
import { repeat } from '../utils/strings';
10+
import { DefinitionLink } from 'vscode-languageserver-types';
11+
12+
suite('JSON Find Definitions', () => {
13+
const testFindDefinitionFor = function (value: string, expected: {offset: number, length: number} | null): PromiseLike<void> {
14+
const offset = value.indexOf('|');
15+
value = value.substr(0, offset) + value.substr(offset + 1);
16+
17+
const ls = getLanguageService({ clientCapabilities: ClientCapabilities.LATEST });
18+
const document = TextDocument.create('test://test/test.json', 'json', 0, value);
19+
const position = Position.create(0, offset);
20+
const jsonDoc = ls.parseJSONDocument(document);
21+
return ls.findDefinition(document, position, jsonDoc).then(list => {
22+
if (expected) {
23+
assert.notDeepEqual(list, []);
24+
const startOffset = list[0].targetRange.start.character;
25+
assert.equal(startOffset, expected.offset);
26+
assert.equal(list[0].targetRange.end.character - startOffset, expected.length);
27+
} else {
28+
assert.deepEqual(list, []);
29+
}
30+
});
31+
};
32+
33+
test('FindDefinition invalid ref', async function () {
34+
await testFindDefinitionFor('{|}', null);
35+
await testFindDefinitionFor('{"name": |"John"}', null);
36+
await testFindDefinitionFor('{"|name": "John"}', null);
37+
await testFindDefinitionFor('{"name": "|John"}', null);
38+
await testFindDefinitionFor('{"name": "John", "$ref": "#/|john/name"}', null);
39+
await testFindDefinitionFor('{"name": "John", "$ref|": "#/name"}', null);
40+
await testFindDefinitionFor('{"name": "John", "$ref": "#/|"}', null);
41+
});
42+
43+
test('FindDefinition valid ref', async function () {
44+
await testFindDefinitionFor('{"name": "John", "$ref": "#/n|ame"}', {offset: 9, length: 6});
45+
await testFindDefinitionFor('{"name": "John", "$ref": "|#/name"}', {offset: 9, length: 6});
46+
await testFindDefinitionFor('{"name": "John", "$ref": |"#/name"}', {offset: 9, length: 6});
47+
await testFindDefinitionFor('{"name": "John", "$ref": "#/name"|}', {offset: 9, length: 6});
48+
await testFindDefinitionFor('{"name": "John", "$ref": "#/name|"}', {offset: 9, length: 6});
49+
await testFindDefinitionFor('{"name": "John", "$ref": "#|"}', {offset: 0, length: 29});
50+
51+
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}"}`;
52+
await testFindDefinitionFor(doc('#'), {offset: 0, length: 105});
53+
await testFindDefinitionFor(doc('#/foo'), {offset: 8, length: 14});
54+
await testFindDefinitionFor(doc('#/foo/0'), {offset: 9, length: 5});
55+
await testFindDefinitionFor(doc('#/foo/1'), {offset: 16, length: 5});
56+
await testFindDefinitionFor(doc('#/foo/01'), null);
57+
await testFindDefinitionFor(doc('#/'), {offset: 27, length: 1});
58+
await testFindDefinitionFor(doc('#/a~1b'), {offset: 36, length: 1});
59+
await testFindDefinitionFor(doc('#/c%d'), {offset: 45, length: 1});
60+
await testFindDefinitionFor(doc('#/e^f'), {offset: 54, length: 1});
61+
await testFindDefinitionFor(doc('#/i\\\\j'), {offset: 64, length: 1});
62+
await testFindDefinitionFor(doc('#/k\\"l'), {offset: 74, length: 1});
63+
await testFindDefinitionFor(doc('#/ '), {offset: 81, length: 1});
64+
await testFindDefinitionFor(doc('#/m~0n'), {offset: 90, length: 1});
65+
});
66+
});

0 commit comments

Comments
 (0)