JSON completion contributors

This commit is contained in:
Martin Aeschlimann
2016-01-25 14:46:25 +01:00
parent 1a7316771f
commit 0587f20cd7
9 changed files with 712 additions and 50 deletions

View File

@@ -10,45 +10,52 @@ import Parser = require('./jsonParser');
import SchemaService = require('./jsonSchemaService');
import JsonSchema = require('./json-toolbox/jsonSchema');
import nls = require('./utils/nls');
import {IJSONWorkerContribution} from './jsonContributions';
import {CompletionItem, CompletionItemKind, CompletionOptions, ITextDocument, TextDocumentIdentifier, TextDocumentPosition, Range, TextEdit} from 'vscode-languageserver';
export interface ISuggestionsCollector {
add(suggestion: CompletionItem): void;
error(message: string): void;
error(message:string): void;
setAsIncomplete(): void;
}
export class JSONCompletion {
private schemaService: SchemaService.IJSONSchemaService;
private contributions: IJSONWorkerContribution[];
constructor(schemaService: SchemaService.IJSONSchemaService) {
constructor(schemaService: SchemaService.IJSONSchemaService, contributions: IJSONWorkerContribution[] = []) {
this.schemaService = schemaService;
this.contributions = contributions;
}
public doSuggest(document: ITextDocument, textDocumentPosition: TextDocumentPosition, doc: Parser.JSONDocument): Thenable<CompletionItem[]> {
var offset = document.offsetAt(textDocumentPosition.position);
var node = doc.getNodeFromOffsetEndInclusive(offset);
var overwriteRange = null;
var result: CompletionItem[] = [];
let offset = document.offsetAt(textDocumentPosition.position);
let node = doc.getNodeFromOffsetEndInclusive(offset);
let overwriteRange = null;
let result: CompletionItem[] = [];
if (node && (node.type === 'string' || node.type === 'number' || node.type === 'boolean' || node.type === 'null')) {
overwriteRange = Range.create(document.positionAt(node.start), document.positionAt(node.end));
}
var proposed: { [key: string]: boolean } = {};
var collector: ISuggestionsCollector = {
let proposed: { [key: string]: boolean } = {};
let collector: ISuggestionsCollector = {
add: (suggestion: CompletionItem) => {
if (!proposed[suggestion.label]) {
proposed[suggestion.label] = true;
if (overwriteRange) {
suggestion.textEdit = TextEdit.replace(overwriteRange, suggestion.insertText);
}
result.push(suggestion);
}
},
setAsIncomplete: () => {
},
error: (message: string) => {
console.log(message);
@@ -56,17 +63,21 @@ export class JSONCompletion {
};
return this.schemaService.getSchemaForResource(textDocumentPosition.uri, doc).then((schema) => {
var addValue = true;
var currentKey = '';
var currentProperty: Parser.PropertyASTNode = null;
let collectionPromises: Thenable<any>[] = [];
let addValue = true;
let currentKey = '';
let currentWord = '';
let currentProperty: Parser.PropertyASTNode = null;
if (node) {
if (node.type === 'string') {
var stringNode = <Parser.StringASTNode>node;
let stringNode = <Parser.StringASTNode>node;
if (stringNode.isKey) {
addValue = !(node.parent && ((<Parser.PropertyASTNode>node.parent).value));
currentProperty = node.parent ? <Parser.PropertyASTNode>node.parent : null;
currentKey = document.getText().substr(node.start + 1, node.end - node.start - 1);
currentKey = document.getText().substring(node.start + 1, node.end - 1);
currentWord = document.getText().substring(node.start + 1, offset);
if (node.parent) {
node = node.parent.parent;
}
@@ -82,21 +93,29 @@ export class JSONCompletion {
return result;
}
// don't suggest properties that are already present
var properties = (<Parser.ObjectASTNode>node).properties;
let properties = (<Parser.ObjectASTNode>node).properties;
properties.forEach(p => {
if (!currentProperty || currentProperty !== p) {
proposed[p.key.value] = true;
}
});
let isLast = properties.length === 0 || offset >= properties[properties.length - 1].start;
if (schema) {
// property proposals with schema
var isLast = properties.length === 0 || offset >= properties[properties.length - 1].start;
this.getPropertySuggestions(schema, doc, node, currentKey, addValue, isLast, collector);
} else if (node.parent) {
// property proposals without schema
this.getSchemaLessPropertySuggestions(doc, node, collector);
}
let location = node.getNodeLocation();
this.contributions.forEach((contribution) => {
let collectPromise = contribution.collectPropertySuggestions(textDocumentPosition.uri, location, this.getCurrentWord(document, offset), addValue, isLast, collector);
if (collectPromise) {
collectionPromises.push(collectPromise);
}
});
}
@@ -112,23 +131,43 @@ export class JSONCompletion {
// value proposals without schema
this.getSchemaLessValueSuggestions(doc, node, offset, document, collector);
}
if (!node) {
this.contributions.forEach((contribution) => {
let collectPromise = contribution.collectDefaultSuggestions(textDocumentPosition.uri, collector);
if (collectPromise) {
collectionPromises.push(collectPromise);
}
});
} else {
if ((node.type === 'property') && offset > (<Parser.PropertyASTNode> node).colonOffset) {
let parentKey = (<Parser.PropertyASTNode>node).key.value;
return result;
let valueNode = (<Parser.PropertyASTNode> node).value;
if (!valueNode || offset <= valueNode.end) {
let location = node.parent.getNodeLocation();
this.contributions.forEach((contribution) => {
let collectPromise = contribution.collectValueSuggestions(textDocumentPosition.uri, location, parentKey, collector);
if (collectPromise) {
collectionPromises.push(collectPromise);
}
});
}
}
}
return Promise.all(collectionPromises).then(() => result );
});
}
private getPropertySuggestions(schema: SchemaService.ResolvedSchema, doc: Parser.JSONDocument, node: Parser.ASTNode, currentWord: string, addValue: boolean, isLast: boolean, collector: ISuggestionsCollector): void {
var matchingSchemas: Parser.IApplicableSchema[] = [];
let matchingSchemas: Parser.IApplicableSchema[] = [];
doc.validate(schema.schema, matchingSchemas, node.start);
matchingSchemas.forEach((s) => {
if (s.node === node && !s.inverted) {
var schemaProperties = s.schema.properties;
let schemaProperties = s.schema.properties;
if (schemaProperties) {
Object.keys(schemaProperties).forEach((key: string) => {
var propertySchema = schemaProperties[key];
let propertySchema = schemaProperties[key];
collector.add({ kind: CompletionItemKind.Property, label: key, insertText: this.getSnippetForProperty(key, propertySchema, addValue, isLast), documentation: propertySchema.description || '' });
});
}
@@ -137,15 +176,15 @@ export class JSONCompletion {
}
private getSchemaLessPropertySuggestions(doc: Parser.JSONDocument, node: Parser.ASTNode, collector: ISuggestionsCollector): void {
var collectSuggestionsForSimilarObject = (obj: Parser.ObjectASTNode) => {
let collectSuggestionsForSimilarObject = (obj: Parser.ObjectASTNode) => {
obj.properties.forEach((p) => {
var key = p.key.value;
let key = p.key.value;
collector.add({ kind: CompletionItemKind.Property, label: key, insertText: this.getSnippetForSimilarProperty(key, p.value), documentation: '' });
});
};
if (node.parent.type === 'property') {
// if the object is a property value, check the tree for other objects that hang under a property of the same name
var parentKey = (<Parser.PropertyASTNode>node.parent).key.value;
let parentKey = (<Parser.PropertyASTNode>node.parent).key.value;
doc.visit((n) => {
if (n.type === 'property' && (<Parser.PropertyASTNode>n).key.value === parentKey && (<Parser.PropertyASTNode>n).value && (<Parser.PropertyASTNode>n).value.type === 'object') {
collectSuggestionsForSimilarObject(<Parser.ObjectASTNode>(<Parser.PropertyASTNode>n).value);
@@ -163,8 +202,8 @@ export class JSONCompletion {
}
private getSchemaLessValueSuggestions(doc: Parser.JSONDocument, node: Parser.ASTNode, offset: number, document: ITextDocument, collector: ISuggestionsCollector): void {
var collectSuggestionsForValues = (value: Parser.ASTNode) => {
var content = this.getMatchingSnippet(value, document);
let collectSuggestionsForValues = (value: Parser.ASTNode) => {
let content = this.getMatchingSnippet(value, document);
collector.add({ kind: this.getSuggestionKind(value.type), label: content, insertText: content, documentation: '' });
if (value.type === 'boolean') {
this.addBooleanSuggestion(!value.getValue(), collector);
@@ -176,12 +215,12 @@ export class JSONCompletion {
collector.add({ kind: this.getSuggestionKind('array'), label: 'Empty array', insertText: '[\n\t{{}}\n]', documentation: '' });
} else {
if (node.type === 'property' && offset > (<Parser.PropertyASTNode>node).colonOffset) {
var valueNode = (<Parser.PropertyASTNode>node).value;
let valueNode = (<Parser.PropertyASTNode>node).value;
if (valueNode && offset > valueNode.end) {
return;
}
// suggest values at the same key
var parentKey = (<Parser.PropertyASTNode>node).key.value;
let parentKey = (<Parser.PropertyASTNode>node).key.value;
doc.visit((n) => {
if (n.type === 'property' && (<Parser.PropertyASTNode>n).key.value === parentKey && (<Parser.PropertyASTNode>n).value) {
collectSuggestionsForValues((<Parser.PropertyASTNode>n).value);
@@ -192,7 +231,7 @@ export class JSONCompletion {
if (node.type === 'array') {
if (node.parent && node.parent.type === 'property') {
// suggest items of an array at the same key
var parentKey = (<Parser.PropertyASTNode>node.parent).key.value;
let parentKey = (<Parser.PropertyASTNode>node.parent).key.value;
doc.visit((n) => {
if (n.type === 'property' && (<Parser.PropertyASTNode>n).key.value === parentKey && (<Parser.PropertyASTNode>n).value && (<Parser.PropertyASTNode>n).value.type === 'array') {
((<Parser.ArrayASTNode>(<Parser.PropertyASTNode>n).value).items).forEach((n) => {
@@ -217,9 +256,9 @@ export class JSONCompletion {
if (!node) {
this.addDefaultSuggestion(schema.schema, collector);
} else {
var parentKey: string = null;
let parentKey: string = null;
if (node && (node.type === 'property') && offset > (<Parser.PropertyASTNode>node).colonOffset) {
var valueNode = (<Parser.PropertyASTNode>node).value;
let valueNode = (<Parser.PropertyASTNode>node).value;
if (valueNode && offset > valueNode.end) {
return; // we are past the value node
}
@@ -227,7 +266,7 @@ export class JSONCompletion {
node = node.parent;
}
if (node && (parentKey !== null || node.type === 'array')) {
var matchingSchemas: Parser.IApplicableSchema[] = [];
let matchingSchemas: Parser.IApplicableSchema[] = [];
doc.validate(schema.schema, matchingSchemas, node.start);
matchingSchemas.forEach((s) => {
@@ -237,7 +276,7 @@ export class JSONCompletion {
this.addEnumSuggestion(s.schema.items, collector);
}
if (s.schema.properties) {
var propertySchema = s.schema.properties[parentKey];
let propertySchema = s.schema.properties[parentKey];
if (propertySchema) {
this.addDefaultSuggestion(propertySchema, collector);
this.addEnumSuggestion(propertySchema, collector);
@@ -293,7 +332,7 @@ export class JSONCompletion {
}
private getLabelForValue(value: any): string {
var label = JSON.stringify(value);
let label = JSON.stringify(value);
label = label.replace('{{', '').replace('}}', '');
if (label.length > 57) {
return label.substr(0, 57).trim() + '...';
@@ -302,7 +341,7 @@ export class JSONCompletion {
}
private getSnippetForValue(value: any): string {
var snippet = JSON.stringify(value, null, '\t');
let snippet = JSON.stringify(value, null, '\t');
switch (typeof value) {
case 'object':
if (value === null) {
@@ -320,7 +359,7 @@ export class JSONCompletion {
private getSuggestionKind(type: any): CompletionItemKind {
if (Array.isArray(type)) {
var array = <any[]>type;
let array = <any[]>type;
type = array.length > 0 ? array[0] : null;
}
if (!type) {
@@ -342,20 +381,20 @@ export class JSONCompletion {
case 'object':
return '{}';
default:
var content = document.getText().substr(node.start, node.end - node.start);
let content = document.getText().substr(node.start, node.end - node.start);
return content;
}
}
private getSnippetForProperty(key: string, propertySchema: JsonSchema.IJSONSchema, addValue: boolean, isLast: boolean): string {
var result = '"' + key + '"';
let result = '"' + key + '"';
if (!addValue) {
return result;
}
result += ': ';
var defaultVal = propertySchema.default;
let defaultVal = propertySchema.default;
if (typeof defaultVal !== 'undefined') {
result = result + this.getSnippetForValue(defaultVal);
} else if (propertySchema.enum && propertySchema.enum.length > 0) {
@@ -393,4 +432,13 @@ export class JSONCompletion {
private getSnippetForSimilarProperty(key: string, templateValue: Parser.ASTNode): string {
return '"' + key + '"';
}
}
private getCurrentWord(document: ITextDocument, offset: number) {
var i = offset - 1;
var text = document.getText();
while (i >= 0 && ' \t\n\r\v"'.indexOf(text.charAt(i)) === -1) {
i--;
}
return text.substring(i+1, offset);
}
}