mirror of
https://github.com/danbulant/monaco-yaml
synced 2026-06-20 06:51:12 +00:00
906 lines
34 KiB
TypeScript
906 lines
34 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Red Hat, Inc. All rights reserved.
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
'use strict';
|
|
|
|
|
|
import * as Parser from '../parser/jsonParser';
|
|
import * as Json from 'jsonc-parser';
|
|
import * as SchemaService from './jsonSchemaService';
|
|
import { JSONSchema, JSONSchemaRef } from '../jsonSchema';
|
|
import { JSONWorkerContribution, CompletionsCollector } from '../jsonContributions';
|
|
import { Thenable } from 'vscode-json-languageservice';
|
|
|
|
import { CompletionItem, CompletionItemKind, CompletionList, TextDocument, Position, Range, TextEdit, InsertTextFormat, MarkupContent, MarkupKind } from 'vscode-languageserver-types';
|
|
|
|
import * as nls from 'vscode-nls';
|
|
import { PropertyASTNode, StringASTNode, ObjectASTNode, ASTNode, ArrayASTNode, ClientCapabilities } from '../jsonLanguageTypes';
|
|
import { endsWith } from '../utils/strings';
|
|
import { isDefined } from '../utils/objects';
|
|
import { stringifyObject } from '../utils/json';
|
|
import { YAMLDocument } from '../yamlLanguageTypes';
|
|
import { matchOffsetToDocument } from '../utils/arrUtils';
|
|
const localize = nls.loadMessageBundle();
|
|
|
|
// !! FIXME: this implementation is buggy
|
|
|
|
export class YAMLCompletion {
|
|
|
|
private supportsMarkdown: boolean | undefined;
|
|
|
|
constructor(private schemaService: SchemaService.IJSONSchemaService, private contributions: JSONWorkerContribution[] = [], private clientCapabilities: ClientCapabilities = {}) {
|
|
this.schemaService = schemaService;
|
|
this.contributions = contributions;
|
|
}
|
|
|
|
public doResolve(item: CompletionItem): Thenable<CompletionItem> {
|
|
for (let i = this.contributions.length - 1; i >= 0; i--) {
|
|
if (this.contributions[i].resolveCompletion) {
|
|
let resolver = this.contributions[i].resolveCompletion(item);
|
|
if (resolver) {
|
|
return resolver;
|
|
}
|
|
}
|
|
}
|
|
return Promise.resolve(item);
|
|
}
|
|
|
|
public doComplete(document: TextDocument, position: Position, doc: YAMLDocument): Thenable<CompletionList> {
|
|
|
|
let result: CompletionList = {
|
|
items: [],
|
|
isIncomplete: false
|
|
};
|
|
|
|
let offset = document.offsetAt(position);
|
|
|
|
let currentDoc = matchOffsetToDocument(offset, doc);
|
|
|
|
if(currentDoc === null){
|
|
return Promise.resolve(result);
|
|
}
|
|
|
|
let node = currentDoc.getNodeFromOffset(offset, true);
|
|
if (this.isInComment(document, node ? node.offset : 0, offset)) {
|
|
return Promise.resolve(result);
|
|
}
|
|
|
|
let currentWord = this.getCurrentWord(document, offset);
|
|
let overwriteRange = null;
|
|
|
|
if (node && (node.type === 'string' || node.type === 'number' || node.type === 'boolean' || node.type === 'null')) {
|
|
overwriteRange = Range.create(document.positionAt(node.offset), document.positionAt(node.offset + node.length));
|
|
} else {
|
|
let overwriteStart = offset - currentWord.length;
|
|
if (overwriteStart > 0 && document.getText()[overwriteStart - 1] === '"') {
|
|
overwriteStart--;
|
|
}
|
|
overwriteRange = Range.create(document.positionAt(overwriteStart), position);
|
|
}
|
|
|
|
let proposed: { [key: string]: CompletionItem } = {};
|
|
let collector: CompletionsCollector = {
|
|
add: (suggestion: CompletionItem) => {
|
|
let existing = proposed[suggestion.label];
|
|
if (!existing) {
|
|
proposed[suggestion.label] = suggestion;
|
|
if (overwriteRange) {
|
|
suggestion.textEdit = TextEdit.replace(overwriteRange, suggestion.insertText);
|
|
}
|
|
|
|
result.items.push(suggestion);
|
|
} else if (!existing.documentation) {
|
|
existing.documentation = suggestion.documentation;
|
|
}
|
|
},
|
|
setAsIncomplete: () => {
|
|
result.isIncomplete = true;
|
|
},
|
|
error: (message: string) => {
|
|
console.error(message);
|
|
},
|
|
log: (message: string) => {
|
|
console.log(message);
|
|
},
|
|
getNumberOfProposals: () => {
|
|
return result.items.length;
|
|
}
|
|
};
|
|
|
|
return this.schemaService.getSchemaForResource(document.uri, currentDoc).then((schema) => {
|
|
let collectionPromises: Thenable<any>[] = [];
|
|
|
|
let addValue = true;
|
|
let currentKey = '';
|
|
|
|
let currentProperty: PropertyASTNode = null;
|
|
if (node) {
|
|
|
|
if (node.type === 'string') {
|
|
let parent = node.parent;
|
|
if (parent && parent.type === 'property' && parent.keyNode === node) {
|
|
addValue = !parent.valueNode;
|
|
currentProperty = parent;
|
|
currentKey = document.getText().substr(node.offset + 1, node.length - 2);
|
|
if (parent) {
|
|
node = parent.parent;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// proposals for properties
|
|
if (node && node.type === 'object') {
|
|
// don't suggest keys when the cursor is just before the opening curly brace
|
|
if (node.offset === offset) {
|
|
return result;
|
|
}
|
|
// don't suggest properties that are already present
|
|
let properties = node.properties;
|
|
properties.forEach(p => {
|
|
if (!currentProperty || currentProperty !== p) {
|
|
proposed[p.keyNode.value] = CompletionItem.create('__');
|
|
}
|
|
});
|
|
let separatorAfter = '';
|
|
if (addValue) {
|
|
separatorAfter = this.evaluateSeparatorAfter(document, document.offsetAt(overwriteRange.end));
|
|
}
|
|
|
|
if (schema) {
|
|
// property proposals with schema
|
|
this.getPropertyCompletions(schema, currentDoc, node, addValue, separatorAfter, collector);
|
|
} else {
|
|
// property proposals without schema
|
|
this.getSchemaLessPropertyCompletions(currentDoc, node, currentKey, collector);
|
|
}
|
|
|
|
let location = Parser.getNodePath(node);
|
|
this.contributions.forEach((contribution) => {
|
|
let collectPromise = contribution.collectPropertyCompletions(document.uri, location, currentWord, addValue, separatorAfter === '', collector);
|
|
if (collectPromise) {
|
|
collectionPromises.push(collectPromise);
|
|
}
|
|
});
|
|
if ((!schema && currentWord.length > 0 && document.getText().charAt(offset - currentWord.length - 1) !== '"')) {
|
|
collector.add({
|
|
kind: CompletionItemKind.Property,
|
|
label: this.getLabelForValue(currentWord),
|
|
insertText: this.getInsertTextForProperty(currentWord, null, false, separatorAfter),
|
|
insertTextFormat: InsertTextFormat.Snippet, documentation: ''
|
|
});
|
|
}
|
|
}
|
|
|
|
// proposals for values
|
|
let types: { [type: string]: boolean } = {};
|
|
if (schema) {
|
|
// value proposals with schema
|
|
this.getValueCompletions(schema, currentDoc, node, offset, document, collector, types);
|
|
} else {
|
|
// value proposals without schema
|
|
this.getSchemaLessValueCompletions(currentDoc, node, offset, document, collector);
|
|
}
|
|
if (this.contributions.length > 0) {
|
|
this.getContributedValueCompletions(currentDoc, node, offset, document, collector, collectionPromises);
|
|
}
|
|
|
|
return Promise.all(collectionPromises).then(() => {
|
|
if (collector.getNumberOfProposals() === 0) {
|
|
let offsetForSeparator = offset;
|
|
if (node && (node.type === 'string' || node.type === 'number' || node.type === 'boolean' || node.type === 'null')) {
|
|
offsetForSeparator = node.offset + node.length;
|
|
}
|
|
let separatorAfter = this.evaluateSeparatorAfter(document, offsetForSeparator);
|
|
this.addFillerValueCompletions(types, separatorAfter, collector);
|
|
}
|
|
return result;
|
|
});
|
|
});
|
|
}
|
|
|
|
private getPropertyCompletions(schema: SchemaService.ResolvedSchema, doc: Parser.JSONDocument, node: ASTNode, addValue: boolean, separatorAfter: string, collector: CompletionsCollector): void {
|
|
let matchingSchemas = doc.getMatchingSchemas(schema.schema, node.offset);
|
|
matchingSchemas.forEach((s) => {
|
|
if (s.node === node && !s.inverted) {
|
|
let schemaProperties = s.schema.properties;
|
|
if (schemaProperties) {
|
|
Object.keys(schemaProperties).forEach((key: string) => {
|
|
let propertySchema = schemaProperties[key];
|
|
if (typeof propertySchema === 'object' && !propertySchema.deprecationMessage && !propertySchema.doNotSuggest) {
|
|
let proposal: CompletionItem = {
|
|
kind: CompletionItemKind.Property,
|
|
label: key,
|
|
insertText: this.getInsertTextForProperty(key, propertySchema, addValue, separatorAfter),
|
|
insertTextFormat: InsertTextFormat.Snippet,
|
|
filterText: this.getFilterTextForValue(key),
|
|
documentation: this.fromMarkup(propertySchema.markdownDescription) || propertySchema.description || '',
|
|
};
|
|
if (endsWith(proposal.insertText, `$1${separatorAfter}`)) {
|
|
proposal.command = {
|
|
title: 'Suggest',
|
|
command: 'editor.action.triggerSuggest'
|
|
};
|
|
}
|
|
collector.add(proposal);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Error fix
|
|
// If this is a array of string/boolean/number
|
|
// test:
|
|
// - item1
|
|
// it will treated as a property key since `:` has been appended
|
|
if (node.type === 'object' && node.parent && node.parent.type === 'array' && s.schema.type !== 'object') {
|
|
this.addSchemaValueCompletions(s.schema, separatorAfter, collector, {});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
private getSchemaLessPropertyCompletions(doc: Parser.JSONDocument, node: ASTNode, currentKey: string, collector: CompletionsCollector): void {
|
|
let collectCompletionsForSimilarObject = (obj: ObjectASTNode) => {
|
|
obj.properties.forEach((p) => {
|
|
let key = p.keyNode.value;
|
|
collector.add({
|
|
kind: CompletionItemKind.Property,
|
|
label: key,
|
|
insertText: this.getInsertTextForValue(key, ''),
|
|
insertTextFormat: InsertTextFormat.Snippet,
|
|
filterText: this.getFilterTextForValue(key),
|
|
documentation: ''
|
|
});
|
|
});
|
|
};
|
|
if (node.parent) {
|
|
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
|
|
let parentKey = node.parent.keyNode.value;
|
|
doc.visit(n => {
|
|
if (n.type === 'property' && n !== node.parent && n.keyNode.value === parentKey && n.valueNode && n.valueNode.type === 'object') {
|
|
collectCompletionsForSimilarObject(n.valueNode);
|
|
}
|
|
return true;
|
|
});
|
|
} else if (node.parent.type === 'array') {
|
|
// if the object is in an array, use all other array elements as similar objects
|
|
node.parent.items.forEach(n => {
|
|
if (n.type === 'object' && n !== node) {
|
|
collectCompletionsForSimilarObject(n);
|
|
}
|
|
});
|
|
}
|
|
} else if (node.type === 'object') {
|
|
collector.add({
|
|
kind: CompletionItemKind.Property,
|
|
label: '$schema',
|
|
insertText: this.getInsertTextForProperty('$schema', null, true, ''),
|
|
insertTextFormat: InsertTextFormat.Snippet, documentation: '',
|
|
filterText: this.getFilterTextForValue("$schema")
|
|
});
|
|
}
|
|
}
|
|
|
|
private getSchemaLessValueCompletions(doc: Parser.JSONDocument, node: ASTNode, offset: number, document: TextDocument, collector: CompletionsCollector): void {
|
|
let offsetForSeparator = offset;
|
|
if (node && (node.type === 'string' || node.type === 'number' || node.type === 'boolean' || node.type === 'null')) {
|
|
offsetForSeparator = node.offset + node.length;
|
|
node = node.parent;
|
|
}
|
|
|
|
if (!node) {
|
|
collector.add({
|
|
kind: this.getSuggestionKind('object'),
|
|
label: 'Empty object',
|
|
insertText: this.getInsertTextForValue({}, ''),
|
|
insertTextFormat: InsertTextFormat.Snippet,
|
|
documentation: ''
|
|
});
|
|
collector.add({
|
|
kind: this.getSuggestionKind('array'),
|
|
label: 'Empty array',
|
|
insertText: this.getInsertTextForValue([], ''),
|
|
insertTextFormat: InsertTextFormat.Snippet,
|
|
documentation: ''
|
|
});
|
|
return;
|
|
}
|
|
let separatorAfter = this.evaluateSeparatorAfter(document, offsetForSeparator);
|
|
let collectSuggestionsForValues = (value: ASTNode) => {
|
|
if (!Parser.contains(value.parent, offset, true)) {
|
|
collector.add({
|
|
kind: this.getSuggestionKind(value.type),
|
|
label: this.getLabelTextForMatchingNode(value, document),
|
|
insertText: this.getInsertTextForMatchingNode(value, document, separatorAfter),
|
|
insertTextFormat: InsertTextFormat.Snippet, documentation: ''
|
|
});
|
|
}
|
|
if (value.type === 'boolean') {
|
|
this.addBooleanValueCompletion(!value.value, separatorAfter, collector);
|
|
}
|
|
};
|
|
|
|
if (node.type === 'property') {
|
|
if (offset > node.colonOffset) {
|
|
|
|
let valueNode = node.valueNode;
|
|
if (valueNode && (offset > (valueNode.offset + valueNode.length) || valueNode.type === 'object' || valueNode.type === 'array')) {
|
|
return;
|
|
}
|
|
// suggest values at the same key
|
|
let parentKey = node.keyNode.value;
|
|
doc.visit(n => {
|
|
if (n.type === 'property' && n.keyNode.value === parentKey && n.valueNode) {
|
|
collectSuggestionsForValues(n.valueNode);
|
|
}
|
|
return true;
|
|
});
|
|
if (parentKey === '$schema' && node.parent && !node.parent.parent) {
|
|
this.addDollarSchemaCompletions(separatorAfter, collector);
|
|
}
|
|
}
|
|
}
|
|
if (node.type === 'array') {
|
|
if (node.parent && node.parent.type === 'property') {
|
|
|
|
// suggest items of an array at the same key
|
|
let parentKey = node.parent.keyNode.value;
|
|
doc.visit((n) => {
|
|
if (n.type === 'property' && n.keyNode.value === parentKey && n.valueNode && n.valueNode.type === 'array') {
|
|
n.valueNode.items.forEach(collectSuggestionsForValues);
|
|
}
|
|
return true;
|
|
});
|
|
} else {
|
|
// suggest items in the same array
|
|
node.items.forEach(collectSuggestionsForValues);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private getValueCompletions(schema: SchemaService.ResolvedSchema, doc: Parser.JSONDocument, node: ASTNode, offset: number, document: TextDocument, collector: CompletionsCollector, types: { [type: string]: boolean }): void {
|
|
let offsetForSeparator = offset;
|
|
let parentKey: string = null;
|
|
let valueNode: ASTNode = null;
|
|
|
|
if (node && (node.type === 'string' || node.type === 'number' || node.type === 'boolean' || node.type === 'null')) {
|
|
offsetForSeparator = node.offset + node.length;
|
|
valueNode = node;
|
|
node = node.parent;
|
|
}
|
|
|
|
if (!node) {
|
|
this.addSchemaValueCompletions(schema.schema, '', collector, types);
|
|
return;
|
|
}
|
|
|
|
if ((node.type === 'property') && offset > node.colonOffset) {
|
|
let valueNode = node.valueNode;
|
|
if (valueNode && offset > (valueNode.offset + valueNode.length)) {
|
|
return; // we are past the value node
|
|
}
|
|
parentKey = node.keyNode.value;
|
|
node = node.parent;
|
|
}
|
|
|
|
if (node && (parentKey !== null || node.type === 'array')) {
|
|
let separatorAfter = this.evaluateSeparatorAfter(document, offsetForSeparator);
|
|
|
|
let matchingSchemas = doc.getMatchingSchemas(schema.schema, node.offset, valueNode);
|
|
matchingSchemas.forEach(s => {
|
|
if (s.node === node && !s.inverted && s.schema) {
|
|
if (node.type === 'array' && s.schema.items) {
|
|
if (Array.isArray(s.schema.items)) {
|
|
let index = this.findItemAtOffset(node, document, offset);
|
|
if (index < s.schema.items.length) {
|
|
this.addSchemaValueCompletions(s.schema.items[index], separatorAfter, collector, types);
|
|
}
|
|
} else {
|
|
this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types);
|
|
}
|
|
}
|
|
if (s.schema.properties) {
|
|
let propertySchema = s.schema.properties[parentKey];
|
|
if (propertySchema) {
|
|
this.addSchemaValueCompletions(propertySchema, separatorAfter, collector, types);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
if (parentKey === '$schema' && !node.parent) {
|
|
this.addDollarSchemaCompletions(separatorAfter, collector);
|
|
}
|
|
if (types['boolean']) {
|
|
this.addBooleanValueCompletion(true, separatorAfter, collector);
|
|
this.addBooleanValueCompletion(false, separatorAfter, collector);
|
|
}
|
|
if (types['null']) {
|
|
this.addNullValueCompletion(separatorAfter, collector);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private getContributedValueCompletions(doc: Parser.JSONDocument, node: ASTNode, offset: number, document: TextDocument, collector: CompletionsCollector, collectionPromises: Thenable<any>[]) {
|
|
if (!node) {
|
|
this.contributions.forEach((contribution) => {
|
|
let collectPromise = contribution.collectDefaultCompletions(document.uri, collector);
|
|
if (collectPromise) {
|
|
collectionPromises.push(collectPromise);
|
|
}
|
|
});
|
|
} else {
|
|
if (node.type === 'string' || node.type === 'number' || node.type === 'boolean' || node.type === 'null') {
|
|
node = node.parent;
|
|
}
|
|
if ((node.type === 'property') && offset > node.colonOffset) {
|
|
let parentKey = node.keyNode.value;
|
|
|
|
let valueNode = node.valueNode;
|
|
if (!valueNode || offset <= (valueNode.offset + valueNode.length)) {
|
|
let location = Parser.getNodePath(node.parent);
|
|
this.contributions.forEach((contribution) => {
|
|
let collectPromise = contribution.collectValueCompletions(document.uri, location, parentKey, collector);
|
|
if (collectPromise) {
|
|
collectionPromises.push(collectPromise);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private addSchemaValueCompletions(schema: JSONSchemaRef, separatorAfter: string, collector: CompletionsCollector, types: { [type: string]: boolean }): void {
|
|
if (typeof schema === 'object') {
|
|
this.addEnumValueCompletions(schema, separatorAfter, collector);
|
|
this.addDefaultValueCompletions(schema, separatorAfter, collector);
|
|
this.collectTypes(schema, types);
|
|
if (Array.isArray(schema.allOf)) {
|
|
schema.allOf.forEach(s => this.addSchemaValueCompletions(s, separatorAfter, collector, types));
|
|
}
|
|
if (Array.isArray(schema.anyOf)) {
|
|
schema.anyOf.forEach(s => this.addSchemaValueCompletions(s, separatorAfter, collector, types));
|
|
}
|
|
if (Array.isArray(schema.oneOf)) {
|
|
schema.oneOf.forEach(s => this.addSchemaValueCompletions(s, separatorAfter, collector, types));
|
|
}
|
|
}
|
|
}
|
|
|
|
private addDefaultValueCompletions(schema: JSONSchema, separatorAfter: string, collector: CompletionsCollector, arrayDepth = 0): void {
|
|
let hasProposals = false;
|
|
if (isDefined(schema.default)) {
|
|
let type = schema.type;
|
|
let value = schema.default;
|
|
for (let i = arrayDepth; i > 0; i--) {
|
|
value = [value];
|
|
type = 'array';
|
|
}
|
|
collector.add({
|
|
kind: this.getSuggestionKind(type),
|
|
label: this.getLabelForValue(value),
|
|
insertText: this.getInsertTextForValue(value, separatorAfter),
|
|
insertTextFormat: InsertTextFormat.Snippet,
|
|
detail: localize('json.suggest.default', 'Default value')
|
|
});
|
|
hasProposals = true;
|
|
}
|
|
if (Array.isArray(schema.examples)) {
|
|
schema.examples.forEach(example => {
|
|
let type = schema.type;
|
|
let value = example;
|
|
for (let i = arrayDepth; i > 0; i--) {
|
|
value = [value];
|
|
type = 'array';
|
|
}
|
|
collector.add({
|
|
kind: this.getSuggestionKind(type),
|
|
label: this.getLabelForValue(value),
|
|
insertText: this.getInsertTextForValue(value, separatorAfter),
|
|
insertTextFormat: InsertTextFormat.Snippet
|
|
});
|
|
hasProposals = true;
|
|
});
|
|
}
|
|
if (Array.isArray(schema.defaultSnippets)) {
|
|
schema.defaultSnippets.forEach(s => {
|
|
let type = schema.type;
|
|
let value = s.body;
|
|
let label = s.label;
|
|
let insertText: string;
|
|
let filterText: string;
|
|
if (isDefined(value)) {
|
|
let type = schema.type;
|
|
for (let i = arrayDepth; i > 0; i--) {
|
|
value = [value];
|
|
type = 'array';
|
|
}
|
|
insertText = this.getInsertTextForSnippetValue(value, separatorAfter);
|
|
filterText = this.getFilterTextForSnippetValue(value);
|
|
label = label || this.getLabelForSnippetValue(value);
|
|
} else if (typeof s.bodyText === 'string') {
|
|
let prefix = '', suffix = '', indent = '';
|
|
for (let i = arrayDepth; i > 0; i--) {
|
|
prefix = prefix + indent + '[\n';
|
|
suffix = suffix + '\n' + indent + ']';
|
|
indent += '\t';
|
|
type = 'array';
|
|
}
|
|
insertText = prefix + indent + s.bodyText.split('\n').join('\n' + indent) + suffix + separatorAfter;
|
|
label = label || insertText;
|
|
filterText = insertText.replace(/[\n]/g, ''); // remove new lines
|
|
}
|
|
collector.add({
|
|
kind: this.getSuggestionKind(type),
|
|
label,
|
|
documentation: this.fromMarkup(s.markdownDescription) || s.description,
|
|
insertText,
|
|
insertTextFormat: InsertTextFormat.Snippet,
|
|
filterText
|
|
});
|
|
hasProposals = true;
|
|
});
|
|
}
|
|
if (!hasProposals && typeof schema.items === 'object' && !Array.isArray(schema.items)) {
|
|
this.addDefaultValueCompletions(schema.items, separatorAfter, collector, arrayDepth + 1);
|
|
}
|
|
}
|
|
|
|
|
|
private addEnumValueCompletions(schema: JSONSchema, separatorAfter: string, collector: CompletionsCollector): void {
|
|
if (isDefined(schema.const)) {
|
|
collector.add({
|
|
kind: this.getSuggestionKind(schema.type),
|
|
label: this.getLabelForValue(schema.const),
|
|
insertText: this.getInsertTextForValue(schema.const, separatorAfter),
|
|
insertTextFormat: InsertTextFormat.Snippet,
|
|
documentation: this.fromMarkup(schema.markdownDescription) || schema.description
|
|
});
|
|
}
|
|
|
|
if (Array.isArray(schema.enum)) {
|
|
for (let i = 0, length = schema.enum.length; i < length; i++) {
|
|
let enm = schema.enum[i];
|
|
let documentation: string | MarkupContent = this.fromMarkup(schema.markdownDescription) || schema.description;
|
|
if (schema.markdownEnumDescriptions && i < schema.markdownEnumDescriptions.length && this.doesSupportMarkdown()) {
|
|
documentation = this.fromMarkup(schema.markdownEnumDescriptions[i]);
|
|
} else if (schema.enumDescriptions && i < schema.enumDescriptions.length) {
|
|
documentation = schema.enumDescriptions[i];
|
|
}
|
|
collector.add({
|
|
kind: this.getSuggestionKind(schema.type),
|
|
label: this.getLabelForValue(enm),
|
|
insertText: this.getInsertTextForValue(enm, separatorAfter),
|
|
insertTextFormat: InsertTextFormat.Snippet,
|
|
documentation
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
private collectTypes(schema: JSONSchema, types: { [type: string]: boolean }) {
|
|
if (Array.isArray(schema.enum) || isDefined(schema.const)) {
|
|
return;
|
|
}
|
|
let type = schema.type;
|
|
if (Array.isArray(type)) {
|
|
type.forEach(t => types[t] = true);
|
|
} else {
|
|
types[type] = true;
|
|
}
|
|
}
|
|
|
|
private addFillerValueCompletions(types: { [type: string]: boolean }, separatorAfter: string, collector: CompletionsCollector): void {
|
|
if (types['object']) {
|
|
collector.add({
|
|
kind: this.getSuggestionKind('object'),
|
|
label: '{}',
|
|
insertText: this.getInsertTextForGuessedValue({}, separatorAfter),
|
|
insertTextFormat: InsertTextFormat.Snippet,
|
|
detail: localize('defaults.object', 'New object'),
|
|
documentation: ''
|
|
});
|
|
}
|
|
if (types['array']) {
|
|
collector.add({
|
|
kind: this.getSuggestionKind('array'),
|
|
label: '[]',
|
|
insertText: this.getInsertTextForGuessedValue([], separatorAfter),
|
|
insertTextFormat: InsertTextFormat.Snippet,
|
|
detail: localize('defaults.array', 'New array'),
|
|
documentation: ''
|
|
});
|
|
}
|
|
}
|
|
|
|
private addBooleanValueCompletion(value: boolean, separatorAfter: string, collector: CompletionsCollector): void {
|
|
collector.add({
|
|
kind: this.getSuggestionKind('boolean'),
|
|
label: value ? 'true' : 'false',
|
|
insertText: this.getInsertTextForValue(value, separatorAfter),
|
|
insertTextFormat: InsertTextFormat.Snippet,
|
|
documentation: ''
|
|
});
|
|
}
|
|
|
|
private addNullValueCompletion(separatorAfter: string, collector: CompletionsCollector): void {
|
|
collector.add({
|
|
kind: this.getSuggestionKind('null'),
|
|
label: 'null',
|
|
insertText: 'null' + separatorAfter,
|
|
insertTextFormat: InsertTextFormat.Snippet,
|
|
documentation: ''
|
|
});
|
|
}
|
|
|
|
private addDollarSchemaCompletions(separatorAfter: string, collector: CompletionsCollector): void {
|
|
let schemaIds = this.schemaService.getRegisteredSchemaIds(schema => schema === 'http' || schema === 'https');
|
|
schemaIds.forEach(schemaId => collector.add({
|
|
kind: CompletionItemKind.Module,
|
|
label: this.getLabelForValue(schemaId),
|
|
filterText: this.getFilterTextForValue(schemaId),
|
|
insertText: this.getInsertTextForValue(schemaId, separatorAfter),
|
|
insertTextFormat: InsertTextFormat.Snippet, documentation: ''
|
|
}));
|
|
}
|
|
|
|
private getLabelForValue(value: any): string {
|
|
let label = JSON.stringify(value);
|
|
if (label.length > 57) {
|
|
return label.substr(0, 57).trim() + '...';
|
|
}
|
|
return label;
|
|
}
|
|
|
|
private getFilterTextForValue(value): string {
|
|
return JSON.stringify(value);
|
|
}
|
|
|
|
private getFilterTextForSnippetValue(value): string {
|
|
return JSON.stringify(value).replace(/\$\{\d+:([^}]+)\}|\$\d+/g, '$1');
|
|
}
|
|
|
|
private getLabelForSnippetValue(value: any): string {
|
|
let label = JSON.stringify(value);
|
|
label = label.replace(/\$\{\d+:([^}]+)\}|\$\d+/g, '$1');
|
|
if (label.length > 57) {
|
|
return label.substr(0, 57).trim() + '...';
|
|
}
|
|
return label;
|
|
}
|
|
|
|
private getInsertTextForPlainText(text: string): string {
|
|
return text.replace(/[\\\$\}]/g, '\\$&'); // escape $, \ and }
|
|
}
|
|
|
|
private getInsertTextForValue(value: any, separatorAfter: string): string {
|
|
var text = JSON.stringify(value, null, '\t');
|
|
if (text === '{}') {
|
|
return '{$1}' + separatorAfter;
|
|
} else if (text === '[]') {
|
|
return '[$1]' + separatorAfter;
|
|
}
|
|
return this.getInsertTextForPlainText(text + separatorAfter);
|
|
}
|
|
|
|
private getInsertTextForSnippetValue(value: any, separatorAfter: string): string {
|
|
let replacer = (value) => {
|
|
if (typeof value === 'string') {
|
|
if (value[0] === '^') {
|
|
return value.substr(1);
|
|
}
|
|
}
|
|
return JSON.stringify(value);
|
|
};
|
|
return stringifyObject(value, '', replacer) + separatorAfter;
|
|
}
|
|
|
|
private templateVarIdCounter = 0;
|
|
|
|
private getInsertTextForGuessedValue(value: any, separatorAfter: string): string {
|
|
switch (typeof value) {
|
|
case 'object':
|
|
if (value === null) {
|
|
return '${1:null}' + separatorAfter;
|
|
}
|
|
return this.getInsertTextForValue(value, separatorAfter);
|
|
case 'string':
|
|
let snippetValue = JSON.stringify(value);
|
|
snippetValue = snippetValue.substr(1, snippetValue.length - 2); // remove quotes
|
|
snippetValue = this.getInsertTextForPlainText(snippetValue); // escape \ and }
|
|
return '"${1:' + snippetValue + '}"' + separatorAfter;
|
|
case 'number':
|
|
case 'boolean':
|
|
return '${1:' + JSON.stringify(value) + '}' + separatorAfter;
|
|
}
|
|
return this.getInsertTextForValue(value, separatorAfter);
|
|
}
|
|
|
|
private getSuggestionKind(type: any): CompletionItemKind {
|
|
if (Array.isArray(type)) {
|
|
let array = <any[]>type;
|
|
type = array.length > 0 ? array[0] : null;
|
|
}
|
|
if (!type) {
|
|
return CompletionItemKind.Value;
|
|
}
|
|
switch (type) {
|
|
case 'string': return CompletionItemKind.Value;
|
|
case 'object': return CompletionItemKind.Module;
|
|
case 'property': return CompletionItemKind.Property;
|
|
default: return CompletionItemKind.Value;
|
|
}
|
|
}
|
|
|
|
private getLabelTextForMatchingNode(node: ASTNode, document: TextDocument): string {
|
|
switch (node.type) {
|
|
case 'array':
|
|
return '[]';
|
|
case 'object':
|
|
return '{}';
|
|
default:
|
|
let content = document.getText().substr(node.offset, node.length);
|
|
return content;
|
|
}
|
|
}
|
|
|
|
private getInsertTextForMatchingNode(node: ASTNode, document: TextDocument, separatorAfter: string): string {
|
|
switch (node.type) {
|
|
case 'array':
|
|
return this.getInsertTextForValue([], separatorAfter);
|
|
case 'object':
|
|
return this.getInsertTextForValue({}, separatorAfter);
|
|
default:
|
|
let content = document.getText().substr(node.offset, node.length) + separatorAfter;
|
|
return this.getInsertTextForPlainText(content);
|
|
}
|
|
}
|
|
|
|
private getInsertTextForProperty(key: string, propertySchema: JSONSchema, addValue: boolean, separatorAfter: string): string {
|
|
|
|
let propertyText = this.getInsertTextForValue(key, '');
|
|
if (!addValue) {
|
|
return propertyText;
|
|
}
|
|
let resultText = propertyText + ': ';
|
|
|
|
let value;
|
|
let nValueProposals = 0;
|
|
if (propertySchema) {
|
|
if (Array.isArray(propertySchema.defaultSnippets)) {
|
|
if (propertySchema.defaultSnippets.length === 1) {
|
|
let body = propertySchema.defaultSnippets[0].body;
|
|
if (isDefined(body)) {
|
|
value = this.getInsertTextForSnippetValue(body, '');
|
|
}
|
|
}
|
|
nValueProposals += propertySchema.defaultSnippets.length;
|
|
}
|
|
if (propertySchema.enum) {
|
|
if (!value && propertySchema.enum.length === 1) {
|
|
value = this.getInsertTextForGuessedValue(propertySchema.enum[0], '');
|
|
}
|
|
nValueProposals += propertySchema.enum.length;
|
|
}
|
|
if (isDefined(propertySchema.default)) {
|
|
if (!value) {
|
|
value = this.getInsertTextForGuessedValue(propertySchema.default, '');
|
|
}
|
|
nValueProposals++;
|
|
}
|
|
if (nValueProposals === 0) {
|
|
var type = Array.isArray(propertySchema.type) ? propertySchema.type[0] : propertySchema.type;
|
|
if (!type) {
|
|
if (propertySchema.properties) {
|
|
type = 'object';
|
|
} else if (propertySchema.items) {
|
|
type = 'array';
|
|
}
|
|
}
|
|
switch (type) {
|
|
case 'boolean':
|
|
value = '$1';
|
|
break;
|
|
case 'string':
|
|
value = '"$1"';
|
|
break;
|
|
case 'object':
|
|
value = '{$1}';
|
|
break;
|
|
case 'array':
|
|
value = '[$1]';
|
|
break;
|
|
case 'number':
|
|
case 'integer':
|
|
value = '${1:0}';
|
|
break;
|
|
case 'null':
|
|
value = '${1:null}';
|
|
break;
|
|
default:
|
|
return propertyText;
|
|
}
|
|
}
|
|
}
|
|
if (!value || nValueProposals > 1) {
|
|
value = '$1';
|
|
}
|
|
return resultText + value + separatorAfter;
|
|
}
|
|
|
|
private getCurrentWord(document: TextDocument, 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);
|
|
}
|
|
|
|
private evaluateSeparatorAfter(document: TextDocument, offset: number) {
|
|
let scanner = Json.createScanner(document.getText(), true);
|
|
scanner.setPosition(offset);
|
|
let token = scanner.scan();
|
|
switch (token) {
|
|
case Json.SyntaxKind.CommaToken:
|
|
case Json.SyntaxKind.CloseBraceToken:
|
|
case Json.SyntaxKind.CloseBracketToken:
|
|
case Json.SyntaxKind.EOF:
|
|
return '';
|
|
default:
|
|
return ',';
|
|
}
|
|
}
|
|
|
|
private findItemAtOffset(node: ArrayASTNode, document: TextDocument, offset: number) {
|
|
let scanner = Json.createScanner(document.getText(), true);
|
|
let children = node.items;
|
|
for (let i = children.length - 1; i >= 0; i--) {
|
|
let child = children[i];
|
|
if (offset > child.offset + child.length) {
|
|
scanner.setPosition(child.offset + child.length);
|
|
let token = scanner.scan();
|
|
if (token === Json.SyntaxKind.CommaToken && offset >= scanner.getTokenOffset() + scanner.getTokenLength()) {
|
|
return i + 1;
|
|
}
|
|
return i;
|
|
} else if (offset >= child.offset) {
|
|
return i;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
private isInComment(document: TextDocument, start: number, offset: number) {
|
|
let scanner = Json.createScanner(document.getText(), false);
|
|
scanner.setPosition(start);
|
|
let token = scanner.scan();
|
|
while (token !== Json.SyntaxKind.EOF && (scanner.getTokenOffset() + scanner.getTokenLength() < offset)) {
|
|
token = scanner.scan();
|
|
}
|
|
return (token === Json.SyntaxKind.LineCommentTrivia || token === Json.SyntaxKind.BlockCommentTrivia) && scanner.getTokenOffset() <= offset;
|
|
}
|
|
|
|
private fromMarkup(markupString: string | undefined): MarkupContent | string | undefined {
|
|
if (markupString && this.doesSupportMarkdown()) {
|
|
return {
|
|
kind: MarkupKind.Markdown,
|
|
value: markupString
|
|
};
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
private doesSupportMarkdown() {
|
|
if (!isDefined(this.supportsMarkdown)) {
|
|
const completion = this.clientCapabilities.textDocument && this.clientCapabilities.textDocument.completion;
|
|
this.supportsMarkdown = completion && completion.completionItem && Array.isArray(completion.completionItem.documentationFormat) && completion.completionItem.documentationFormat.indexOf(MarkupKind.Markdown) !== -1;
|
|
}
|
|
return this.supportsMarkdown;
|
|
}
|
|
}
|