vscode-json-languageservice
Version:
Language service for JSON
969 lines (968 loc) • 45.4 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as Parser from '../parser/jsonParser';
import * as Json from 'jsonc-parser';
import { stringifyObject } from '../utils/json';
import { endsWith, extendedRegExp } from '../utils/strings';
import { isDefined } from '../utils/objects';
import { CompletionItem, CompletionItemKind, Range, TextEdit, InsertTextFormat, MarkupKind } from '../jsonLanguageTypes';
import * as l10n from '@vscode/l10n';
const valueCommitCharacters = [',', '}', ']'];
const propertyCommitCharacters = [':'];
export class JSONCompletion {
constructor(schemaService, contributions = [], promiseConstructor = Promise, clientCapabilities = {}) {
this.schemaService = schemaService;
this.contributions = contributions;
this.promiseConstructor = promiseConstructor;
this.clientCapabilities = clientCapabilities;
}
doResolve(item) {
for (let i = this.contributions.length - 1; i >= 0; i--) {
const resolveCompletion = this.contributions[i].resolveCompletion;
if (resolveCompletion) {
const resolver = resolveCompletion(item);
if (resolver) {
return resolver;
}
}
}
return this.promiseConstructor.resolve(item);
}
doComplete(document, position, doc) {
const result = {
items: [],
isIncomplete: false
};
const text = document.getText();
const offset = document.offsetAt(position);
let node = doc.getNodeFromOffset(offset, true);
if (this.isInComment(document, node ? node.offset : 0, offset)) {
return Promise.resolve(result);
}
if (node && (offset === node.offset + node.length) && offset > 0) {
const ch = text[offset - 1];
if (node.type === 'object' && ch === '}' || node.type === 'array' && ch === ']') {
// after ] or }
node = node.parent;
}
}
const currentWord = this.getCurrentWord(document, offset);
let overwriteRange;
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 && text[overwriteStart - 1] === '"') {
overwriteStart--;
}
overwriteRange = Range.create(document.positionAt(overwriteStart), position);
}
const supportsCommitCharacters = false; //this.doesSupportsCommitCharacters(); disabled for now, waiting for new API: https://github.com/microsoft/vscode/issues/42544
const proposed = new Map();
const collector = {
add: (suggestion) => {
let label = suggestion.label;
const existing = proposed.get(label);
if (!existing) {
label = label.replace(/[\n]/g, '↵');
if (label.length > 60) {
const shortendedLabel = label.substr(0, 57).trim() + '...';
if (!proposed.has(shortendedLabel)) {
label = shortendedLabel;
}
}
suggestion.textEdit = TextEdit.replace(overwriteRange, suggestion.insertText);
if (supportsCommitCharacters) {
suggestion.commitCharacters = suggestion.kind === CompletionItemKind.Property ? propertyCommitCharacters : valueCommitCharacters;
}
suggestion.label = label;
proposed.set(label, suggestion);
result.items.push(suggestion);
}
else {
if (!existing.documentation) {
existing.documentation = suggestion.documentation;
}
if (!existing.detail) {
existing.detail = suggestion.detail;
}
if (!existing.labelDetails) {
existing.labelDetails = suggestion.labelDetails;
}
}
},
setAsIncomplete: () => {
result.isIncomplete = true;
},
error: (message) => {
console.error(message);
},
getNumberOfProposals: () => {
return result.items.length;
}
};
return this.schemaService.getSchemaForResource(document.uri, doc).then((schema) => {
const collectionPromises = [];
let addValue = true;
let currentKey = '';
let currentProperty = undefined;
if (node) {
if (node.type === 'string') {
const parent = node.parent;
if (parent && parent.type === 'property' && parent.keyNode === node) {
addValue = !parent.valueNode;
currentProperty = parent;
currentKey = text.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
const properties = node.properties;
properties.forEach(p => {
if (!currentProperty || currentProperty !== p) {
proposed.set(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, doc, node, addValue, separatorAfter, collector);
}
else {
// property proposals without schema
this.getSchemaLessPropertyCompletions(doc, node, currentKey, collector);
}
const location = Parser.getNodePath(node);
this.contributions.forEach((contribution) => {
const collectPromise = contribution.collectPropertyCompletions(document.uri, location, currentWord, addValue, separatorAfter === '', collector);
if (collectPromise) {
collectionPromises.push(collectPromise);
}
});
if ((!schema && currentWord.length > 0 && text.charAt(offset - currentWord.length - 1) !== '"')) {
collector.add({
kind: CompletionItemKind.Property,
label: this.getLabelForValue(currentWord),
insertText: this.getInsertTextForProperty(currentWord, undefined, false, separatorAfter),
insertTextFormat: InsertTextFormat.Snippet, documentation: '',
});
collector.setAsIncomplete();
}
}
// proposals for values
const types = {};
if (schema) {
// value proposals with schema
this.getValueCompletions(schema, doc, node, offset, document, collector, types);
}
else {
// value proposals without schema
this.getSchemaLessValueCompletions(doc, node, offset, document, collector);
}
if (this.contributions.length > 0) {
this.getContributedValueCompletions(doc, node, offset, document, collector, collectionPromises);
}
return this.promiseConstructor.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;
}
const separatorAfter = this.evaluateSeparatorAfter(document, offsetForSeparator);
this.addFillerValueCompletions(types, separatorAfter, collector);
}
return result;
});
});
}
getPropertyCompletions(schema, doc, node, addValue, separatorAfter, collector) {
const matchingSchemas = doc.getMatchingSchemas(schema.schema, node.offset);
matchingSchemas.forEach((s) => {
if (s.node === node && !s.inverted) {
const schemaProperties = s.schema.properties;
if (schemaProperties) {
Object.keys(schemaProperties).forEach((key) => {
const propertySchema = schemaProperties[key];
if (typeof propertySchema === 'object' && !propertySchema.deprecationMessage && !propertySchema.doNotSuggest) {
const proposal = {
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 (propertySchema.completionDetail !== undefined) {
proposal.detail = propertySchema.completionDetail;
}
if (propertySchema.suggestSortText !== undefined) {
proposal.sortText = propertySchema.suggestSortText;
}
if (proposal.insertText && endsWith(proposal.insertText, `$1${separatorAfter}`)) {
proposal.command = {
title: 'Suggest',
command: 'editor.action.triggerSuggest'
};
}
collector.add(proposal);
}
});
}
const schemaPropertyNames = s.schema.propertyNames;
if (typeof schemaPropertyNames === 'object' && !schemaPropertyNames.deprecationMessage && !schemaPropertyNames.doNotSuggest) {
const propertyNameCompletionItem = (name, documentation, detail, sortText) => {
const proposal = {
kind: CompletionItemKind.Property,
label: name,
insertText: this.getInsertTextForProperty(name, undefined, addValue, separatorAfter),
insertTextFormat: InsertTextFormat.Snippet,
filterText: this.getFilterTextForValue(name),
documentation: documentation || this.fromMarkup(schemaPropertyNames.markdownDescription) || schemaPropertyNames.description || '',
sortText,
detail
};
if (proposal.insertText && endsWith(proposal.insertText, `$1${separatorAfter}`)) {
proposal.command = {
title: 'Suggest',
command: 'editor.action.triggerSuggest'
};
}
collector.add(proposal);
};
if (schemaPropertyNames.enum) {
for (let i = 0; i < schemaPropertyNames.enum.length; i++) {
let enumDescription = undefined;
if (schemaPropertyNames.markdownEnumDescriptions && i < schemaPropertyNames.markdownEnumDescriptions.length) {
enumDescription = this.fromMarkup(schemaPropertyNames.markdownEnumDescriptions[i]);
}
else if (schemaPropertyNames.enumDescriptions && i < schemaPropertyNames.enumDescriptions.length) {
enumDescription = schemaPropertyNames.enumDescriptions[i];
}
const enumSortText = schemaPropertyNames.enumSortTexts?.[i];
const enumDetails = schemaPropertyNames.enumDetails?.[i];
propertyNameCompletionItem(schemaPropertyNames.enum[i], enumDescription, enumDetails, enumSortText);
}
}
if (schemaPropertyNames.examples) {
for (let i = 0; i < schemaPropertyNames.examples.length; i++) {
propertyNameCompletionItem(schemaPropertyNames.examples[i], undefined, undefined, undefined);
}
}
if (schemaPropertyNames.const) {
propertyNameCompletionItem(schemaPropertyNames.const, undefined, schemaPropertyNames.completionDetail, schemaPropertyNames.suggestSortText);
}
}
}
});
}
getSchemaLessPropertyCompletions(doc, node, currentKey, collector) {
const collectCompletionsForSimilarObject = (obj) => {
obj.properties.forEach((p) => {
const 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
const 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', undefined, true, ''),
insertTextFormat: InsertTextFormat.Snippet, documentation: '',
filterText: this.getFilterTextForValue("$schema")
});
}
}
getSchemaLessValueCompletions(doc, node, offset, document, collector) {
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;
}
const separatorAfter = this.evaluateSeparatorAfter(document, offsetForSeparator);
const collectSuggestionsForValues = (value) => {
if (value.parent && !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 || 0)) {
const valueNode = node.valueNode;
if (valueNode && (offset > (valueNode.offset + valueNode.length) || valueNode.type === 'object' || valueNode.type === 'array')) {
return;
}
// suggest values at the same key
const 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
const 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);
}
}
}
getValueCompletions(schema, doc, node, offset, document, collector, types) {
let offsetForSeparator = offset;
let parentKey = undefined;
let valueNode = undefined;
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 || 0)) {
const 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 !== undefined || node.type === 'array')) {
const separatorAfter = this.evaluateSeparatorAfter(document, offsetForSeparator);
const matchingSchemas = doc.getMatchingSchemas(schema.schema, node.offset, valueNode);
for (const s of matchingSchemas) {
if (s.node === node && !s.inverted && s.schema) {
if (node.type === 'array' && s.schema.items) {
let c = collector;
if (s.schema.uniqueItems) {
const existingValues = new Set();
node.children.forEach(n => {
if (n.type !== 'array' && n.type !== 'object') {
existingValues.add(this.getLabelForValue(Parser.getNodeValue(n)));
}
});
c = {
...collector,
add(suggestion) {
if (!existingValues.has(suggestion.label)) {
collector.add(suggestion);
}
}
};
}
if (Array.isArray(s.schema.items)) {
const index = this.findItemAtOffset(node, document, offset);
if (index < s.schema.items.length) {
this.addSchemaValueCompletions(s.schema.items[index], separatorAfter, c, types);
}
}
else {
this.addSchemaValueCompletions(s.schema.items, separatorAfter, c, types);
}
}
if (parentKey !== undefined) {
let propertyMatched = false;
if (s.schema.properties) {
const propertySchema = s.schema.properties[parentKey];
if (propertySchema) {
propertyMatched = true;
this.addSchemaValueCompletions(propertySchema, separatorAfter, collector, types);
}
}
if (s.schema.patternProperties && !propertyMatched) {
for (const pattern of Object.keys(s.schema.patternProperties)) {
const regex = extendedRegExp(pattern);
if (regex?.test(parentKey)) {
propertyMatched = true;
const propertySchema = s.schema.patternProperties[pattern];
this.addSchemaValueCompletions(propertySchema, separatorAfter, collector, types);
}
}
}
if (s.schema.additionalProperties && !propertyMatched) {
const propertySchema = s.schema.additionalProperties;
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);
}
}
}
getContributedValueCompletions(doc, node, offset, document, collector, collectionPromises) {
if (!node) {
this.contributions.forEach((contribution) => {
const 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 && (node.type === 'property') && offset > (node.colonOffset || 0)) {
const parentKey = node.keyNode.value;
const valueNode = node.valueNode;
if ((!valueNode || offset <= (valueNode.offset + valueNode.length)) && node.parent) {
const location = Parser.getNodePath(node.parent);
this.contributions.forEach((contribution) => {
const collectPromise = contribution.collectValueCompletions(document.uri, location, parentKey, collector);
if (collectPromise) {
collectionPromises.push(collectPromise);
}
});
}
}
}
}
addSchemaValueCompletions(schema, separatorAfter, collector, types) {
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));
}
}
}
addDefaultValueCompletions(schema, separatorAfter, collector, arrayDepth = 0) {
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';
}
const completionItem = {
kind: this.getSuggestionKind(type),
label: this.getLabelForValue(value),
insertText: this.getInsertTextForValue(value, separatorAfter),
insertTextFormat: InsertTextFormat.Snippet
};
if (this.doesSupportsLabelDetails()) {
completionItem.labelDetails = { description: l10n.t('Default value') };
}
else {
completionItem.detail = l10n.t('Default value');
}
collector.add(completionItem);
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;
let filterText;
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
}
else {
return;
}
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) && arrayDepth < 5 /* beware of recursion */) {
this.addDefaultValueCompletions(schema.items, separatorAfter, collector, arrayDepth + 1);
}
}
addEnumValueCompletions(schema, separatorAfter, collector) {
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++) {
const enm = schema.enum[i];
let documentation = 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,
sortText: schema.enumSortTexts?.[i],
detail: schema.enumDetails?.[i],
documentation
});
}
}
}
collectTypes(schema, types) {
if (Array.isArray(schema.enum) || isDefined(schema.const)) {
return;
}
const type = schema.type;
if (Array.isArray(type)) {
type.forEach(t => types[t] = true);
}
else if (type) {
types[type] = true;
}
}
addFillerValueCompletions(types, separatorAfter, collector) {
if (types['object']) {
collector.add({
kind: this.getSuggestionKind('object'),
label: '{}',
insertText: this.getInsertTextForGuessedValue({}, separatorAfter),
insertTextFormat: InsertTextFormat.Snippet,
detail: l10n.t('New object'),
documentation: ''
});
}
if (types['array']) {
collector.add({
kind: this.getSuggestionKind('array'),
label: '[]',
insertText: this.getInsertTextForGuessedValue([], separatorAfter),
insertTextFormat: InsertTextFormat.Snippet,
detail: l10n.t('New array'),
documentation: ''
});
}
}
addBooleanValueCompletion(value, separatorAfter, collector) {
collector.add({
kind: this.getSuggestionKind('boolean'),
label: value ? 'true' : 'false',
insertText: this.getInsertTextForValue(value, separatorAfter),
insertTextFormat: InsertTextFormat.Snippet,
documentation: ''
});
}
addNullValueCompletion(separatorAfter, collector) {
collector.add({
kind: this.getSuggestionKind('null'),
label: 'null',
insertText: 'null' + separatorAfter,
insertTextFormat: InsertTextFormat.Snippet,
documentation: ''
});
}
addDollarSchemaCompletions(separatorAfter, collector) {
const schemaIds = this.schemaService.getRegisteredSchemaIds(schema => schema === 'http' || schema === 'https');
schemaIds.forEach(schemaId => {
if (schemaId.startsWith('https://json-schema.org/draft-')) {
schemaId = schemaId + '#';
}
collector.add({
kind: CompletionItemKind.Module,
label: this.getLabelForValue(schemaId),
filterText: this.getFilterTextForValue(schemaId),
insertText: this.getInsertTextForValue(schemaId, separatorAfter),
insertTextFormat: InsertTextFormat.Snippet, documentation: ''
});
});
}
getLabelForValue(value) {
return JSON.stringify(value);
}
getValueFromLabel(value) {
return JSON.parse(value);
}
getFilterTextForValue(value) {
return JSON.stringify(value);
}
getFilterTextForSnippetValue(value) {
return JSON.stringify(value).replace(/\$\{\d+:([^}]+)\}|\$\d+/g, '$1');
}
getLabelForSnippetValue(value) {
const label = JSON.stringify(value);
return label.replace(/\$\{\d+:([^}]+)\}|\$\d+/g, '$1');
}
getInsertTextForPlainText(text) {
return text.replace(/[\\\$\}]/g, '\\$&'); // escape $, \ and }
}
getInsertTextForValue(value, separatorAfter) {
const text = JSON.stringify(value, null, '\t');
if (text === '{}') {
return '{$1}' + separatorAfter;
}
else if (text === '[]') {
return '[$1]' + separatorAfter;
}
return this.getInsertTextForPlainText(text + separatorAfter);
}
getInsertTextForSnippetValue(value, separatorAfter) {
const replacer = (value) => {
if (typeof value === 'string') {
if (value[0] === '^') {
return value.substr(1);
}
}
return JSON.stringify(value);
};
return stringifyObject(value, '', replacer) + separatorAfter;
}
getInsertTextForGuessedValue(value, separatorAfter) {
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);
}
getSuggestionKind(type) {
if (Array.isArray(type)) {
const array = type;
type = array.length > 0 ? array[0] : undefined;
}
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;
}
}
getLabelTextForMatchingNode(node, document) {
switch (node.type) {
case 'array':
return '[]';
case 'object':
return '{}';
default:
const content = document.getText().substr(node.offset, node.length);
return content;
}
}
getInsertTextForMatchingNode(node, document, separatorAfter) {
switch (node.type) {
case 'array':
return this.getInsertTextForValue([], separatorAfter);
case 'object':
return this.getInsertTextForValue({}, separatorAfter);
default:
const content = document.getText().substr(node.offset, node.length) + separatorAfter;
return this.getInsertTextForPlainText(content);
}
}
getInsertTextForProperty(key, propertySchema, addValue, separatorAfter) {
const propertyText = this.getInsertTextForValue(key, '');
if (!addValue) {
return propertyText;
}
const resultText = propertyText + ': ';
let value;
let nValueProposals = 0;
if (propertySchema) {
if (Array.isArray(propertySchema.defaultSnippets)) {
if (propertySchema.defaultSnippets.length === 1) {
const 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.const)) {
if (!value) {
value = this.getInsertTextForGuessedValue(propertySchema.const, '');
}
nValueProposals++;
}
if (isDefined(propertySchema.default)) {
if (!value) {
value = this.getInsertTextForGuessedValue(propertySchema.default, '');
}
nValueProposals++;
}
if (Array.isArray(propertySchema.examples) && propertySchema.examples.length) {
if (!value) {
value = this.getInsertTextForGuessedValue(propertySchema.examples[0], '');
}
nValueProposals += propertySchema.examples.length;
}
if (nValueProposals === 0) {
let 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;
}
getCurrentWord(document, offset) {
let i = offset - 1;
const text = document.getText();
while (i >= 0 && ' \t\n\r\v":{[,]}'.indexOf(text.charAt(i)) === -1) {
i--;
}
return text.substring(i + 1, offset);
}
evaluateSeparatorAfter(document, offset) {
const scanner = Json.createScanner(document.getText(), true);
scanner.setPosition(offset);
const token = scanner.scan();
switch (token) {
case 5 /* Json.SyntaxKind.CommaToken */:
case 2 /* Json.SyntaxKind.CloseBraceToken */:
case 4 /* Json.SyntaxKind.CloseBracketToken */:
case 17 /* Json.SyntaxKind.EOF */:
return '';
default:
return ',';
}
}
findItemAtOffset(node, document, offset) {
const scanner = Json.createScanner(document.getText(), true);
const children = node.items;
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
if (offset > child.offset + child.length) {
scanner.setPosition(child.offset + child.length);
const token = scanner.scan();
if (token === 5 /* Json.SyntaxKind.CommaToken */ && offset >= scanner.getTokenOffset() + scanner.getTokenLength()) {
return i + 1;
}
return i;
}
else if (offset >= child.offset) {
return i;
}
}
return 0;
}
isInComment(document, start, offset) {
const scanner = Json.createScanner(document.getText(), false);
scanner.setPosition(start);
let token = scanner.scan();
while (token !== 17 /* Json.SyntaxKind.EOF */ && (scanner.getTokenOffset() + scanner.getTokenLength() < offset)) {
token = scanner.scan();
}
return (token === 12 /* Json.SyntaxKind.LineCommentTrivia */ || token === 13 /* Json.SyntaxKind.BlockCommentTrivia */) && scanner.getTokenOffset() <= offset;
}
fromMarkup(markupString) {
if (markupString && this.doesSupportMarkdown()) {
return {
kind: MarkupKind.Markdown,
value: markupString
};
}
return undefined;
}
doesSupportMarkdown() {
if (!isDefined(this.supportsMarkdown)) {
const documentationFormat = this.clientCapabilities.textDocument?.completion?.completionItem?.documentationFormat;
this.supportsMarkdown = Array.isArray(documentationFormat) && documentationFormat.indexOf(MarkupKind.Markdown) !== -1;
}
return this.supportsMarkdown;
}
doesSupportsCommitCharacters() {
if (!isDefined(this.supportsCommitCharacters)) {
this.labelDetailsSupport = this.clientCapabilities.textDocument?.completion?.completionItem?.commitCharactersSupport;
}
return this.supportsCommitCharacters;
}
doesSupportsLabelDetails() {
if (!isDefined(this.labelDetailsSupport)) {
this.labelDetailsSupport = this.clientCapabilities.textDocument?.completion?.completionItem?.labelDetailsSupport;
}
return this.labelDetailsSupport;
}
}