yaml-language-server
Version:
YAML language server
860 lines • 77.6 kB
JavaScript
"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Red Hat, Inc. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.YamlCompletion = void 0;
const vscode_languageserver_types_1 = require("vscode-languageserver-types");
const yaml_1 = require("yaml");
const arrUtils_1 = require("../utils/arrUtils");
const indentationGuesser_1 = require("../utils/indentationGuesser");
const textBuffer_1 = require("../utils/textBuffer");
const json_1 = require("../utils/json");
const objects_1 = require("../utils/objects");
const nls = require("vscode-nls");
const isKubernetes_1 = require("../parser/isKubernetes");
const jsonParser07_1 = require("../parser/jsonParser07");
const astUtils_1 = require("../utils/astUtils");
const modelineUtil_1 = require("./modelineUtil");
const schemaUtils_1 = require("../utils/schemaUtils");
const localize = nls.loadMessageBundle();
const doubleQuotesEscapeRegExp = /[\\]+"/g;
const parentCompletionKind = vscode_languageserver_types_1.CompletionItemKind.Class;
const existingProposeItem = '__';
class YamlCompletion {
constructor(schemaService, clientCapabilities = {}, yamlDocument, telemetry) {
this.schemaService = schemaService;
this.clientCapabilities = clientCapabilities;
this.yamlDocument = yamlDocument;
this.telemetry = telemetry;
this.completionEnabled = true;
this.arrayPrefixIndentation = '';
this.isNumberExp = /^\d+$/;
}
configure(languageSettings, yamlSettings) {
if (languageSettings) {
this.completionEnabled = languageSettings.completion;
}
this.customTags = languageSettings.customTags;
this.yamlVersion = languageSettings.yamlVersion;
this.isSingleQuote = yamlSettings?.yamlFormatterSettings?.singleQuote || false;
this.configuredIndentation = languageSettings.indentation;
this.disableDefaultProperties = languageSettings.disableDefaultProperties;
this.parentSkeletonSelectedFirst = languageSettings.parentSkeletonSelectedFirst;
}
async doComplete(document, position, isKubernetes = false, doComplete = true) {
const result = vscode_languageserver_types_1.CompletionList.create([], false);
if (!this.completionEnabled) {
return result;
}
const doc = this.yamlDocument.getYamlDocument(document, { customTags: this.customTags, yamlVersion: this.yamlVersion }, true);
const textBuffer = new textBuffer_1.TextBuffer(document);
if (!this.configuredIndentation) {
const indent = (0, indentationGuesser_1.guessIndentation)(textBuffer, 2, true);
this.indentation = indent.insertSpaces ? ' '.repeat(indent.tabSize) : '\t';
}
else {
this.indentation = this.configuredIndentation;
}
(0, isKubernetes_1.setKubernetesParserOption)(doc.documents, isKubernetes);
// set parser options
for (const jsonDoc of doc.documents) {
jsonDoc.uri = document.uri;
}
const offset = document.offsetAt(position);
const text = document.getText();
if (text.charAt(offset - 1) === ':') {
return Promise.resolve(result);
}
let currentDoc = (0, arrUtils_1.matchOffsetToDocument)(offset, doc);
if (currentDoc === null) {
return Promise.resolve(result);
}
// as we modify AST for completion, we need to use copy of original document
currentDoc = currentDoc.clone();
let [node, foundByClosest] = currentDoc.getNodeFromPosition(offset, textBuffer, this.indentation.length);
const currentWord = this.getCurrentWord(document, offset);
let lineContent = textBuffer.getLineContent(position.line);
const lineAfterPosition = lineContent.substring(position.character);
const areOnlySpacesAfterPosition = /^[ ]+\n?$/.test(lineAfterPosition);
this.arrayPrefixIndentation = '';
let overwriteRange = null;
if (areOnlySpacesAfterPosition) {
overwriteRange = vscode_languageserver_types_1.Range.create(position, vscode_languageserver_types_1.Position.create(position.line, lineContent.length));
const isOnlyWhitespace = lineContent.trim().length === 0;
const isOnlyDash = lineContent.match(/^\s*(-)\s*$/);
if (node && (0, yaml_1.isScalar)(node) && !isOnlyWhitespace && !isOnlyDash) {
const lineToPosition = lineContent.substring(0, position.character);
const matches =
// get indentation of unfinished property (between indent and cursor)
lineToPosition.match(/^[\s-]*([^:]+)?$/) ||
// OR get unfinished value (between colon and cursor)
lineToPosition.match(/:[ \t]((?!:[ \t]).*)$/);
if (matches?.[1]) {
overwriteRange = vscode_languageserver_types_1.Range.create(vscode_languageserver_types_1.Position.create(position.line, position.character - matches[1].length), vscode_languageserver_types_1.Position.create(position.line, lineContent.length));
}
}
}
else if (node && (0, yaml_1.isScalar)(node) && node.value === 'null') {
const nodeStartPos = document.positionAt(node.range[0]);
nodeStartPos.character += 1;
const nodeEndPos = document.positionAt(node.range[2]);
nodeEndPos.character += 1;
overwriteRange = vscode_languageserver_types_1.Range.create(nodeStartPos, nodeEndPos);
}
else if (node && (0, yaml_1.isScalar)(node) && node.value) {
const start = document.positionAt(node.range[0]);
overwriteRange = vscode_languageserver_types_1.Range.create(start, document.positionAt(node.range[1]));
}
else if (node && (0, yaml_1.isScalar)(node) && node.value === null && currentWord === '-') {
overwriteRange = vscode_languageserver_types_1.Range.create(position, position);
this.arrayPrefixIndentation = ' ';
}
else {
let overwriteStart = offset - currentWord.length;
if (overwriteStart > 0 && text[overwriteStart - 1] === '"') {
overwriteStart--;
}
overwriteRange = vscode_languageserver_types_1.Range.create(document.positionAt(overwriteStart), position);
}
const proposed = {};
const collector = {
add: (completionItem, oneOfSchema) => {
const addSuggestionForParent = function (completionItem) {
const existsInYaml = proposed[completionItem.label]?.label === existingProposeItem;
//don't put to parent suggestion if already in yaml
if (existsInYaml) {
return;
}
const schema = completionItem.parent.schema;
const schemaType = (0, schemaUtils_1.getSchemaTypeName)(schema);
const schemaDescription = schema.markdownDescription || schema.description;
let parentCompletion = result.items.find((item) => item.parent?.schema === schema && item.kind === parentCompletionKind);
if (parentCompletion && parentCompletion.parent.insertTexts.includes(completionItem.insertText)) {
// already exists in the parent
return;
}
else if (!parentCompletion) {
// create a new parent
parentCompletion = {
...completionItem,
label: schemaType,
documentation: schemaDescription,
sortText: '_' + schemaType,
kind: parentCompletionKind,
};
parentCompletion.label = parentCompletion.label || completionItem.label;
parentCompletion.parent.insertTexts = [completionItem.insertText];
result.items.push(parentCompletion);
}
else {
// add to the existing parent
parentCompletion.parent.insertTexts.push(completionItem.insertText);
}
};
const isForParentCompletion = !!completionItem.parent;
let label = completionItem.label;
if (!label) {
// we receive not valid CompletionItem as `label` is mandatory field, so just ignore it
console.warn(`Ignoring CompletionItem without label: ${JSON.stringify(completionItem)}`);
return;
}
if (!(0, objects_1.isString)(label)) {
label = String(label);
}
label = label.replace(/[\n]/g, '↵');
if (label.length > 60) {
const shortendedLabel = label.substr(0, 57).trim() + '...';
if (!proposed[shortendedLabel]) {
label = shortendedLabel;
}
}
if (completionItem.label.toLowerCase() === 'regular expression') {
const docObject = completionItem.documentation;
const splitValues = docObject.value.split(':');
label =
splitValues.length > 0 ? `${this.getQuote()}\\${JSON.parse(splitValues[1])}${this.getQuote()}` : completionItem.label;
completionItem.insertText = label;
completionItem.textEdit = vscode_languageserver_types_1.TextEdit.replace(overwriteRange, label);
}
else {
let mdText = completionItem.insertText.replace(/\${[0-9]+[:|](.*)}/g, (s, arg) => arg).replace(/\$([0-9]+)/g, '');
const splitMDText = mdText.split(':');
let value = splitMDText.length > 1 ? splitMDText[1].trim() : mdText;
if (value && /^(['\\"\\])$/.test(value)) {
value = `${this.getQuote()}\\${value}${this.getQuote()}`;
mdText = splitMDText.length > 1 ? splitMDText[0] + ': ' + value : value;
completionItem.insertText = mdText;
}
// trim $1 from end of completion
if (completionItem.insertText.endsWith('$1') && !isForParentCompletion) {
completionItem.insertText = completionItem.insertText.substr(0, completionItem.insertText.length - 2);
}
if (overwriteRange && overwriteRange.start.line === overwriteRange.end.line) {
completionItem.textEdit = vscode_languageserver_types_1.TextEdit.replace(overwriteRange, completionItem.insertText);
}
}
completionItem.label = label;
if (isForParentCompletion) {
addSuggestionForParent(completionItem);
return;
}
if (this.arrayPrefixIndentation) {
this.updateCompletionText(completionItem, this.arrayPrefixIndentation + completionItem.insertText);
}
const existing = proposed[label];
const isInsertTextDifferent = existing?.label !== existingProposeItem && existing?.insertText !== completionItem.insertText;
if (!existing) {
proposed[label] = completionItem;
result.items.push(completionItem);
}
else if (isInsertTextDifferent) {
// try to merge simple insert values
const mergedText = this.mergeSimpleInsertTexts(label, existing.insertText, completionItem.insertText, oneOfSchema);
if (mergedText) {
this.updateCompletionText(existing, mergedText);
}
else {
// add to result when it wasn't able to merge (even if the item is already there but with a different value)
proposed[label] = completionItem;
result.items.push(completionItem);
}
}
if (existing && !existing.documentation && completionItem.documentation) {
existing.documentation = completionItem.documentation;
}
},
error: (message) => {
this.telemetry?.sendError('yaml.completion.error', message);
},
log: (message) => {
console.log(message);
},
getNumberOfProposals: () => {
return result.items.length;
},
result,
proposed,
};
if (this.customTags && this.customTags.length > 0) {
this.getCustomTagValueCompletions(collector);
}
if (lineContent.endsWith('\n')) {
lineContent = lineContent.substr(0, lineContent.length - 1);
}
try {
const schema = await this.schemaService.getSchemaForResource(document.uri, currentDoc);
if (!schema || schema.errors.length) {
if (position.line === 0 && position.character === 0 && !(0, modelineUtil_1.isModeline)(lineContent)) {
const inlineSchemaCompletion = {
kind: vscode_languageserver_types_1.CompletionItemKind.Text,
label: 'Inline schema',
insertText: '# yaml-language-server: $schema=',
insertTextFormat: vscode_languageserver_types_1.InsertTextFormat.PlainText,
};
result.items.push(inlineSchemaCompletion);
}
}
if ((0, modelineUtil_1.isModeline)(lineContent) || (0, astUtils_1.isInComment)(doc.tokens, offset)) {
const schemaIndex = lineContent.indexOf('$schema=');
if (schemaIndex !== -1 && schemaIndex + '$schema='.length <= position.character) {
this.schemaService.getAllSchemas().forEach((schema) => {
const schemaIdCompletion = {
kind: vscode_languageserver_types_1.CompletionItemKind.Constant,
label: schema.name ?? schema.uri,
detail: schema.description,
insertText: schema.uri,
insertTextFormat: vscode_languageserver_types_1.InsertTextFormat.PlainText,
insertTextMode: vscode_languageserver_types_1.InsertTextMode.asIs,
};
result.items.push(schemaIdCompletion);
});
}
return result;
}
if (!schema || schema.errors.length) {
return result;
}
let currentProperty = null;
if (!node) {
if (!currentDoc.internalDocument.contents || (0, yaml_1.isScalar)(currentDoc.internalDocument.contents)) {
const map = currentDoc.internalDocument.createNode({});
map.range = [offset, offset + 1, offset + 1];
currentDoc.internalDocument.contents = map;
currentDoc.updateFromInternalDocument();
node = map;
}
else {
node = currentDoc.findClosestNode(offset, textBuffer);
foundByClosest = true;
}
}
const originalNode = node;
if (node) {
if (lineContent.length === 0) {
node = currentDoc.internalDocument.contents;
}
else {
const parent = currentDoc.getParent(node);
if (parent) {
if ((0, yaml_1.isScalar)(node)) {
if (node.value) {
if ((0, yaml_1.isPair)(parent)) {
if (parent.value === node) {
if (lineContent.trim().length > 0 && lineContent.indexOf(':') < 0) {
const map = this.createTempObjNode(currentWord, node, currentDoc);
const parentParent = currentDoc.getParent(parent);
if ((0, yaml_1.isSeq)(currentDoc.internalDocument.contents)) {
const index = (0, astUtils_1.indexOf)(currentDoc.internalDocument.contents, parent);
if (typeof index === 'number') {
currentDoc.internalDocument.set(index, map);
currentDoc.updateFromInternalDocument();
}
}
else if (parentParent && ((0, yaml_1.isMap)(parentParent) || (0, yaml_1.isSeq)(parentParent))) {
parentParent.set(parent.key, map);
currentDoc.updateFromInternalDocument();
}
else {
currentDoc.internalDocument.set(parent.key, map);
currentDoc.updateFromInternalDocument();
}
currentProperty = map.items[0];
node = map;
}
else if (lineContent.trim().length === 0) {
const parentParent = currentDoc.getParent(parent);
if (parentParent) {
node = parentParent;
}
}
}
else if (parent.key === node) {
const parentParent = currentDoc.getParent(parent);
currentProperty = parent;
if (parentParent) {
node = parentParent;
}
}
}
else if ((0, yaml_1.isSeq)(parent)) {
if (lineContent.trim().length > 0) {
const map = this.createTempObjNode(currentWord, node, currentDoc);
parent.delete(node);
parent.add(map);
currentDoc.updateFromInternalDocument();
node = map;
}
else {
node = parent;
}
}
}
else if (node.value === null) {
if ((0, yaml_1.isPair)(parent)) {
if (parent.key === node) {
node = parent;
}
else {
if ((0, yaml_1.isNode)(parent.key) && parent.key.range) {
const parentParent = currentDoc.getParent(parent);
if (foundByClosest && parentParent && (0, yaml_1.isMap)(parentParent) && (0, astUtils_1.isMapContainsEmptyPair)(parentParent)) {
node = parentParent;
}
else {
const parentPosition = document.positionAt(parent.key.range[0]);
//if cursor has bigger indentation that parent key, then we need to complete new empty object
if (position.character > parentPosition.character && position.line !== parentPosition.line) {
const map = this.createTempObjNode(currentWord, node, currentDoc);
if (parentParent && ((0, yaml_1.isMap)(parentParent) || (0, yaml_1.isSeq)(parentParent))) {
parentParent.set(parent.key, map);
currentDoc.updateFromInternalDocument();
}
else {
currentDoc.internalDocument.set(parent.key, map);
currentDoc.updateFromInternalDocument();
}
currentProperty = map.items[0];
node = map;
}
else if (parentPosition.character === position.character) {
if (parentParent) {
node = parentParent;
}
}
}
}
}
}
else if ((0, yaml_1.isSeq)(parent)) {
if (lineContent.charAt(position.character - 1) !== '-') {
const map = this.createTempObjNode(currentWord, node, currentDoc);
parent.delete(node);
parent.add(map);
currentDoc.updateFromInternalDocument();
node = map;
}
else if (lineContent.charAt(position.character - 1) === '-') {
const map = this.createTempObjNode('', node, currentDoc);
parent.delete(node);
parent.add(map);
currentDoc.updateFromInternalDocument();
node = map;
}
else {
node = parent;
}
}
}
}
else if ((0, yaml_1.isMap)(node)) {
if (!foundByClosest && lineContent.trim().length === 0 && (0, yaml_1.isSeq)(parent)) {
const nextLine = textBuffer.getLineContent(position.line + 1);
if (textBuffer.getLineCount() === position.line + 1 || nextLine.trim().length === 0) {
node = parent;
}
}
}
}
else if ((0, yaml_1.isScalar)(node)) {
const map = this.createTempObjNode(currentWord, node, currentDoc);
currentDoc.internalDocument.contents = map;
currentDoc.updateFromInternalDocument();
currentProperty = map.items[0];
node = map;
}
else if ((0, yaml_1.isMap)(node)) {
for (const pair of node.items) {
if ((0, yaml_1.isNode)(pair.value) && pair.value.range && pair.value.range[0] === offset + 1) {
node = pair.value;
}
}
}
else if ((0, yaml_1.isSeq)(node)) {
if (lineContent.charAt(position.character - 1) !== '-') {
const map = this.createTempObjNode(currentWord, node, currentDoc);
map.items = [];
currentDoc.updateFromInternalDocument();
for (const pair of node.items) {
if ((0, yaml_1.isMap)(pair)) {
pair.items.forEach((value) => {
map.items.push(value);
});
}
}
node = map;
}
}
}
}
// completion for object keys
if (node && (0, yaml_1.isMap)(node)) {
// don't suggest properties that are already present
const properties = node.items;
for (const p of properties) {
if (!currentProperty || currentProperty !== p) {
if ((0, yaml_1.isScalar)(p.key)) {
proposed[p.key.value + ''] = vscode_languageserver_types_1.CompletionItem.create(existingProposeItem);
}
}
}
this.addPropertyCompletions(schema, currentDoc, node, originalNode, '', collector, textBuffer, overwriteRange, doComplete);
if (!schema && currentWord.length > 0 && text.charAt(offset - currentWord.length - 1) !== '"') {
collector.add({
kind: vscode_languageserver_types_1.CompletionItemKind.Property,
label: currentWord,
insertText: this.getInsertTextForProperty(currentWord, null, ''),
insertTextFormat: vscode_languageserver_types_1.InsertTextFormat.Snippet,
});
}
}
// proposals for values
const types = {};
this.getValueCompletions(schema, currentDoc, node, offset, document, collector, types, doComplete);
}
catch (err) {
this.telemetry?.sendError('yaml.completion.error', err);
}
this.finalizeParentCompletion(result);
const uniqueItems = result.items.filter((arr, index, self) => index ===
self.findIndex((item) => item.label === arr.label && item.insertText === arr.insertText && item.kind === arr.kind));
if (uniqueItems?.length > 0) {
result.items = uniqueItems;
}
return result;
}
updateCompletionText(completionItem, text) {
completionItem.insertText = text;
if (completionItem.textEdit) {
completionItem.textEdit.newText = text;
}
}
mergeSimpleInsertTexts(label, existingText, addingText, oneOfSchema) {
const containsNewLineAfterColon = (value) => {
return value.includes('\n');
};
const startWithNewLine = (value) => {
return value.startsWith('\n');
};
const isNullObject = (value) => {
const index = value.indexOf('\n');
return index > 0 && value.substring(index, value.length).trim().length === 0;
};
if (containsNewLineAfterColon(existingText) || containsNewLineAfterColon(addingText)) {
//if the exisiting object null one then replace with the non-null object
if (oneOfSchema && isNullObject(existingText) && !isNullObject(addingText) && !startWithNewLine(addingText)) {
return addingText;
}
return undefined;
}
const existingValues = this.getValuesFromInsertText(existingText);
const addingValues = this.getValuesFromInsertText(addingText);
const newValues = Array.prototype.concat(existingValues, addingValues);
if (!newValues.length) {
return undefined;
}
else if (newValues.length === 1) {
return `${label}: \${1:${newValues[0]}}`;
}
else {
return `${label}: \${1|${newValues.join(',')}|}`;
}
}
getValuesFromInsertText(insertText) {
const value = insertText.substring(insertText.indexOf(':') + 1).trim();
if (!value) {
return [];
}
const valueMath = value.match(/^\${1[|:]([^|]*)+\|?}$/); // ${1|one,two,three|} or ${1:one}
if (valueMath) {
return valueMath[1].split(',');
}
return [value];
}
finalizeParentCompletion(result) {
const reindexText = (insertTexts) => {
//modify added props to have unique $x
let max$index = 0;
return insertTexts.map((text) => {
const match = text.match(/\$([0-9]+)|\${[0-9]+:/g);
if (!match) {
return text;
}
const max$indexLocal = match
.map((m) => +m.replace(/\${([0-9]+)[:|]/g, '$1').replace('$', '')) // get numbers form $1 or ${1:...}
.reduce((p, n) => (n > p ? n : p), 0); // find the max one
const reindexedStr = text
.replace(/\$([0-9]+)/g, (s, args) => '$' + (+args + max$index)) // increment each by max$index
.replace(/\${([0-9]+)[:|]/g, (s, args) => '${' + (+args + max$index) + ':'); // increment each by max$index
max$index += max$indexLocal;
return reindexedStr;
});
};
result.items.forEach((completionItem) => {
if (this.isParentCompletionItem(completionItem)) {
const indent = completionItem.parent.indent || '';
const reindexedTexts = reindexText(completionItem.parent.insertTexts);
// add indent to each object property and join completion item texts
let insertText = reindexedTexts.join(`\n${indent}`);
// trim $1 from end of completion
if (insertText.endsWith('$1')) {
insertText = insertText.substring(0, insertText.length - 2);
}
completionItem.insertText = this.arrayPrefixIndentation + insertText;
if (completionItem.textEdit) {
completionItem.textEdit.newText = completionItem.insertText;
}
// remove $x or use {$x:value} in documentation
const mdText = insertText.replace(/\${[0-9]+[:|](.*)}/g, (s, arg) => arg).replace(/\$([0-9]+)/g, '');
const originalDocumentation = completionItem.documentation ? [completionItem.documentation, '', '----', ''] : [];
completionItem.documentation = {
kind: vscode_languageserver_types_1.MarkupKind.Markdown,
value: [...originalDocumentation, '```yaml', indent + mdText, '```'].join('\n'),
};
delete completionItem.parent;
}
});
}
createTempObjNode(currentWord, node, currentDoc) {
const obj = {};
obj[currentWord] = null;
const map = currentDoc.internalDocument.createNode(obj);
map.range = node.range;
map.items[0].key.range = node.range;
map.items[0].value.range = node.range;
return map;
}
addPropertyCompletions(schema, doc, node, originalNode, separatorAfter, collector, textBuffer, overwriteRange, doComplete) {
const matchingSchemas = doc.getMatchingSchemas(schema.schema, -1, null, doComplete);
const existingKey = textBuffer.getText(overwriteRange);
const lineContent = textBuffer.getLineContent(overwriteRange.start.line);
const hasOnlyWhitespace = lineContent.trim().length === 0;
const hasColon = lineContent.indexOf(':') !== -1;
const isInArray = lineContent.trimLeft().indexOf('-') === 0;
const nodeParent = doc.getParent(node);
const matchOriginal = matchingSchemas.find((it) => it.node.internalNode === originalNode && it.schema.properties);
const oneOfSchema = matchingSchemas.filter((schema) => schema.schema.oneOf).map((oneOfSchema) => oneOfSchema.schema.oneOf)[0];
let didOneOfSchemaMatches = false;
if (oneOfSchema?.length < matchingSchemas.length) {
oneOfSchema?.forEach((property, index) => {
if (!matchingSchemas[index]?.schema.oneOf && matchingSchemas[index]?.schema.properties === property.properties) {
didOneOfSchemaMatches = true;
}
});
}
for (const schema of matchingSchemas) {
if (((schema.node.internalNode === node && !matchOriginal) ||
(schema.node.internalNode === originalNode && !hasColon) ||
(schema.node.parent?.internalNode === originalNode && !hasColon)) &&
!schema.inverted) {
this.collectDefaultSnippets(schema.schema, separatorAfter, collector, {
newLineFirst: false,
indentFirstObject: false,
shouldIndentWithTab: isInArray,
});
const schemaProperties = schema.schema.properties;
if (schemaProperties) {
const maxProperties = schema.schema.maxProperties;
if (maxProperties === undefined ||
node.items === undefined ||
node.items.length < maxProperties ||
(node.items.length === maxProperties && !hasOnlyWhitespace)) {
for (const key in schemaProperties) {
if (Object.prototype.hasOwnProperty.call(schemaProperties, key)) {
const propertySchema = schemaProperties[key];
if (typeof propertySchema === 'object' && !propertySchema.deprecationMessage && !propertySchema['doNotSuggest']) {
let identCompensation = '';
if (nodeParent && (0, yaml_1.isSeq)(nodeParent) && node.items.length <= 1 && !hasOnlyWhitespace) {
// because there is a slash '-' to prevent the properties generated to have the correct
// indent
const sourceText = textBuffer.getText();
const indexOfSlash = sourceText.lastIndexOf('-', node.range[0] - 1);
if (indexOfSlash >= 0) {
// add one space to compensate the '-'
const overwriteChars = overwriteRange.end.character - overwriteRange.start.character;
identCompensation = ' ' + sourceText.slice(indexOfSlash + 1, node.range[1] - overwriteChars);
}
}
identCompensation += this.arrayPrefixIndentation;
// if check that current node has last pair with "null" value and key witch match key from schema,
// and if schema has array definition it add completion item for array item creation
let pair;
if (propertySchema.type === 'array' &&
(pair = node.items.find((it) => (0, yaml_1.isScalar)(it.key) &&
it.key.range &&
it.key.value === key &&
(0, yaml_1.isScalar)(it.value) &&
!it.value.value &&
textBuffer.getPosition(it.key.range[2]).line === overwriteRange.end.line - 1)) &&
pair) {
if (Array.isArray(propertySchema.items)) {
this.addSchemaValueCompletions(propertySchema.items[0], separatorAfter, collector, {}, 'property');
}
else if (typeof propertySchema.items === 'object' && propertySchema.items.type === 'object') {
this.addArrayItemValueCompletion(propertySchema.items, separatorAfter, collector);
}
}
let insertText = key;
if (!key.startsWith(existingKey) || !hasColon) {
insertText = this.getInsertTextForProperty(key, propertySchema, separatorAfter, identCompensation + this.indentation);
}
const isNodeNull = ((0, yaml_1.isScalar)(originalNode) && originalNode.value === null) ||
((0, yaml_1.isMap)(originalNode) && originalNode.items.length === 0);
const existsParentCompletion = schema.schema.required?.length > 0;
if (!this.parentSkeletonSelectedFirst || !isNodeNull || !existsParentCompletion) {
collector.add({
kind: vscode_languageserver_types_1.CompletionItemKind.Property,
label: key,
insertText,
insertTextFormat: vscode_languageserver_types_1.InsertTextFormat.Snippet,
documentation: this.fromMarkup(propertySchema.markdownDescription) || propertySchema.description || '',
}, didOneOfSchemaMatches);
}
// if the prop is required add it also to parent suggestion
if (schema.schema.required?.includes(key)) {
collector.add({
label: key,
insertText: this.getInsertTextForProperty(key, propertySchema, separatorAfter, identCompensation + this.indentation),
insertTextFormat: vscode_languageserver_types_1.InsertTextFormat.Snippet,
documentation: this.fromMarkup(propertySchema.markdownDescription) || propertySchema.description || '',
parent: {
schema: schema.schema,
indent: identCompensation,
},
});
}
}
}
}
}
}
// 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 (nodeParent && (0, yaml_1.isSeq)(nodeParent) && (0, schemaUtils_1.isPrimitiveType)(schema.schema)) {
this.addSchemaValueCompletions(schema.schema, separatorAfter, collector, {}, 'property', Array.isArray(nodeParent.items));
}
if (schema.schema.propertyNames && schema.schema.additionalProperties && schema.schema.type === 'object') {
const propertyNameSchema = (0, jsonParser07_1.asSchema)(schema.schema.propertyNames);
if (!propertyNameSchema.deprecationMessage && !propertyNameSchema.doNotSuggest) {
const label = propertyNameSchema.title || 'property';
collector.add({
kind: vscode_languageserver_types_1.CompletionItemKind.Property,
label,
insertText: '$' + `{1:${label}}: `,
insertTextFormat: vscode_languageserver_types_1.InsertTextFormat.Snippet,
documentation: this.fromMarkup(propertyNameSchema.markdownDescription) || propertyNameSchema.description || '',
});
}
}
}
if (nodeParent && schema.node.internalNode === nodeParent && schema.schema.defaultSnippets) {
// For some reason the first item in the array needs to be treated differently, otherwise
// the indentation will not be correct
if (node.items.length === 1) {
this.collectDefaultSnippets(schema.schema, separatorAfter, collector, {
newLineFirst: false,
indentFirstObject: false,
shouldIndentWithTab: true,
}, 1);
}
else {
this.collectDefaultSnippets(schema.schema, separatorAfter, collector, {
newLineFirst: false,
indentFirstObject: true,
shouldIndentWithTab: false,
}, 1);
}
}
}
}
getValueCompletions(schema, doc, node, offset, document, collector, types, doComplete) {
let parentKey = null;
if (node && (0, yaml_1.isScalar)(node)) {
node = doc.getParent(node);
}
if (!node) {
this.addSchemaValueCompletions(schema.schema, '', collector, types, 'value');
return;
}
if ((0, yaml_1.isPair)(node)) {
const valueNode = node.value;
if (valueNode && valueNode.range && offset > valueNode.range[0] + valueNode.range[2]) {
return; // we are past the value node
}
parentKey = (0, yaml_1.isScalar)(node.key) ? node.key.value + '' : null;
node = doc.getParent(node);
}
if (node && (parentKey !== null || (0, yaml_1.isSeq)(node))) {
const separatorAfter = '';
const matchingSchemas = doc.getMatchingSchemas(schema.schema, -1, null, doComplete);
for (const s of matchingSchemas) {
if (s.node.internalNode === node && !s.inverted && s.schema) {
if (s.schema.items) {
this.collectDefaultSnippets(s.schema, separatorAfter, collector, {
newLineFirst: false,
indentFirstObject: false,
shouldIndentWithTab: false,
});
if ((0, yaml_1.isSeq)(node) && node.items) {
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, collector, types, 'value');
}
}
else if (typeof s.schema.items === 'object' &&
(s.schema.items.type === 'object' || (0, schemaUtils_1.isAnyOfAllOfOneOfType)(s.schema.items))) {
this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, 'value', true);
}
else {
this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, 'value');
}
}
}
if (s.schema.properties) {
const propertySchema = s.schema.properties[parentKey];
if (propertySchema) {
this.addSchemaValueCompletions(propertySchema, separatorAfter, collector, types, 'value');
}
}
if (s.schema.additionalProperties) {
this.addSchemaValueCompletions(s.schema.additionalProperties, separatorAfter, collector, types, 'value');
}
}
}
if (types['boolean']) {
this.addBooleanValueCompletion(true, separatorAfter, collector);
this.addBooleanValueCompletion(false, separatorAfter, collector);
}
if (types['null']) {
this.addNullValueCompletion(separatorAfter, collector);
}
}
}
addArrayItemValueCompletion(schema, separatorAfter, collector, index) {
const schemaType = (0, schemaUtils_1.getSchemaTypeName)(schema);
const insertText = `- ${this.getInsertTextForObject(schema, separatorAfter).insertText.trimLeft()}`;
//append insertText to documentation
const schemaTypeTitle = schemaType ? ' type `' + schemaType + '`' : '';
const schemaDescription = schema.description ? ' (' + schema.description + ')' : '';
const documentation = this.getDocumentationWithMarkdownText(`Create an item of an array${schemaTypeTitle}${schemaDescription}`, insertText);
collector.add({
kind: this.getSuggestionKind(schema.type),
label: '- (array item) ' + (schemaType || index),
documentation: documentation,
insertText: insertText,
insertTextFormat: vscode_languageserver_types_1.InsertTextFormat.Snippet,
});
}
getInsertTextForProperty(key, propertySchema, separatorAfter, indent = this.indentation) {
const propertyText = this.getInsertTextForValue(key, '', 'string');
const resultText = propertyText + ':';
let value;
let nValueProposals = 0;
if (propertySchema) {
let type = Array.isArray(propertySchema.type) ? propertySchema.type[0] : propertySchema.type;
if (!type) {
if (propertySchema.properties) {
type = 'object';
}
else if (propertySchema.items) {
type = 'array';
}
else if (propertySchema.anyOf) {
type = 'anyOf';
}
}
if (Array.isArray(propertySchema.defaultSnippets)) {
if (propertySchema.defaultSnippets.length === 1) {
const body = propertySchema.defaultSnippets[0].body;
if ((0, objects_1.isDefined)(body)) {
value = this.getInsertTextForSnippetValue(body, '', {
newLineFirst: true,
indentFirstObject: false,
shouldIndentWithTab: false,
}, [], 1);
// add space before default snippet value
if (!value.startsWith(' ') && !value.startsWith('\n')) {
value = ' ' + value;
}
}
}
nValueProposals += propertySchema.defaultSnippets.length;
}
if (propertySchema.enum) {
if (!value && propertySchema.enum.length === 1) {
value = ' ' + this.getInsertTextForGuessedValue(propertySchema.enum[0], '', type);
}
nValueProposals += propertySchema.enum.length;
}
if (propertySchema.const) {
if (!value) {
value = this.getInsertTextForGuessedValue(propertySchema.const, '', type);
value = this.evaluateTab1Symbol(value); // prevent const being selected after snippet insert
value = ' ' + value;
}
nValue