UNPKG

yaml-language-server

Version:
403 lines 18.4 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * 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. *--------------------------------------------------------------------------------------------*/ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.YAMLHover = void 0; const l10n = __importStar(require("@vscode/l10n")); const path = __importStar(require("path")); const vscode_languageserver_types_1 = require("vscode-languageserver-types"); const vscode_uri_1 = require("vscode-uri"); const yaml_1 = require("yaml"); const isKubernetes_1 = require("../parser/isKubernetes"); const astNodeUtils_1 = require("../utils/astNodeUtils"); const yaml_documents_1 = require("../parser/yaml-documents"); const arrUtils_1 = require("../utils/arrUtils"); const yamlScalar_1 = require("../utils/yamlScalar"); class YAMLHover { constructor(schemaService, telemetry) { this.telemetry = telemetry; this.shouldHover = true; this.schemaService = schemaService; } configure(languageSettings) { if (languageSettings) { this.shouldHover = languageSettings.hover; this.shouldHoverAnchor = languageSettings.hoverAnchor; this.indentation = languageSettings.indentation; this.hoverSchemaSource = languageSettings.hoverSchemaSource ?? true; } } doHover(document, position, isKubernetes = false) { try { if (!this.shouldHover || !document) { return Promise.resolve(undefined); } const doc = yaml_documents_1.yamlDocumentsCache.getYamlDocument(document); const offset = document.offsetAt(position); const currentDoc = (0, arrUtils_1.matchOffsetToDocument)(offset, doc); if (currentDoc === null) { return Promise.resolve(undefined); } (0, isKubernetes_1.setKubernetesParserOption)(doc.documents, isKubernetes); const currentDocIndex = doc.documents.indexOf(currentDoc); currentDoc.currentDocIndex = currentDocIndex; return this.getHover(document, position, currentDoc); } catch (error) { this.telemetry?.sendError('yaml.hover.error', error); } } // method copied from https://github.com/microsoft/vscode-json-languageservice/blob/2ea5ad3d2ffbbe40dea11cfe764a502becf113ce/src/services/jsonHover.ts#L23 async getHover(document, position, doc) { const offset = document.offsetAt(position); let node = doc.getNodeFromOffset(offset); if (!node || ((node.type === 'object' || node.type === 'array') && offset > node.offset + 1 && offset < node.offset + node.length - 1)) { return Promise.resolve(null); } const hoverRangeNode = node; // use the property description when hovering over an object key if (node.type === 'string') { const parent = node.parent; if (parent && parent.type === 'property' && parent.keyNode === node) { node = parent.valueNode; if (!node) { return Promise.resolve(null); } } } const hoverRange = vscode_languageserver_types_1.Range.create(document.positionAt(hoverRangeNode.offset), document.positionAt(hoverRangeNode.offset + hoverRangeNode.length)); const createHover = (contents) => { const markupContent = { kind: vscode_languageserver_types_1.MarkupKind.Markdown, value: contents, }; const result = { contents: markupContent, range: hoverRange, }; return result; }; if (this.shouldHoverAnchor && node.type === 'property' && node.valueNode) { if (node.valueNode.type === 'object') { const resolved = this.resolveMergeKeys(node.valueNode, doc); const contents = '```yaml\n' + (0, yaml_1.stringify)(resolved, null, 2) + '\n```'; return Promise.resolve(createHover(contents)); } } const removePipe = (value) => { return value.replace(/\s\|\|\s*$/, ''); }; const schema = await this.schemaService.getSchemaForResource(document.uri, doc); if (schema && node && !schema.errors.length) { const matchingSchemas = doc.getMatchingSchemas(schema.schema, node.offset); let title = undefined; let markdownDescription = undefined; let markdownEnumDescriptions = []; const markdownExamples = []; const markdownEnums = []; let enumIdx = undefined; matchingSchemas.every((s) => { if ((s.node === node || (node.type === 'property' && node.valueNode === s.node)) && !s.inverted && s.schema) { title = title || s.schema.title || s.schema.closestTitle; markdownDescription = markdownDescription || s.schema.markdownDescription || this.toMarkdown(s.schema.description); if (s.schema.enum) { enumIdx = s.schema.enum.indexOf((0, astNodeUtils_1.getNodeValue)(node)); if (s.schema.markdownEnumDescriptions) { markdownEnumDescriptions = s.schema.markdownEnumDescriptions; } else if (s.schema.enumDescriptions) { markdownEnumDescriptions = s.schema.enumDescriptions.map(this.toMarkdown, this); } else { markdownEnumDescriptions = []; } s.schema.enum.forEach((enumValue, idx) => { var _a; enumValue = typeof enumValue === 'string' ? (0, yamlScalar_1.toYamlStringScalar)(enumValue, false) : JSON.stringify(enumValue); //insert only if the value is not present yet (avoiding duplicates) //but it also adds or keeps the description of the enum value const foundIdx = markdownEnums.findIndex((me) => me.value === enumValue); if (foundIdx < 0) { markdownEnums.push({ value: enumValue, description: markdownEnumDescriptions[idx], }); } else { (_a = markdownEnums[foundIdx]).description || (_a.description = markdownEnumDescriptions[idx]); } }); } if (s.schema.anyOf && isAllSchemasMatched(node, matchingSchemas, s.schema)) { //if append title and description of all matched schemas on hover title = ''; markdownDescription = s.schema.description ? s.schema.description + '\n' : ''; s.schema.anyOf.forEach((childSchema, index) => { title += childSchema.title || s.schema.closestTitle || ''; markdownDescription += childSchema.markdownDescription || this.toMarkdown(childSchema.description) || ''; if (index !== s.schema.anyOf.length - 1) { title += ' || '; markdownDescription += ' || '; } }); title = removePipe(title); markdownDescription = removePipe(markdownDescription); } if (s.schema.examples) { s.schema.examples.forEach((example) => { markdownExamples.push((0, yaml_1.stringify)(example, null, 2)); }); } } return true; }); let result_1 = ''; if (title) { result_1 = '#### ' + this.toMarkdown(title); } if (markdownDescription) { result_1 = ensureLineBreak(result_1); result_1 += markdownDescription; } if (markdownEnums.length !== 0) { result_1 = ensureLineBreak(result_1); result_1 += l10n.t('Allowed Values:') + '\n\n'; if (enumIdx) { markdownEnums.unshift(markdownEnums.splice(enumIdx, 1)[0]); } markdownEnums.forEach((me_1) => { if (me_1.description) { result_1 += `* \`${toMarkdownCodeBlock(me_1.value)}\`: ${me_1.description}\n`; } else { result_1 += `* \`${toMarkdownCodeBlock(me_1.value)}\`\n`; } }); } if (markdownExamples.length !== 0) { markdownExamples.forEach((example_1) => { result_1 = ensureLineBreak(result_1); result_1 += l10n.t('Example:') + '\n\n'; result_1 += `\`\`\`yaml\n${example_1}\`\`\`\n`; }); } if (result_1.length > 0 && schema.schema.url && this.hoverSchemaSource) { result_1 = ensureLineBreak(result_1); result_1 += l10n.t('Source: [{0}]({1})', getSchemaName(schema.schema), schema.schema.url); } return createHover(result_1); } return null; } /** * Resolves merge keys (<<) and anchors recursively in an object node * @param node The object AST node to resolve * @param doc The YAML document for resolving anchors * @param currentRecursionLevel Current recursion level (default: 0) * @returns A plain JavaScript object with all merges resolved */ resolveMergeKeys(node, doc, currentRecursionLevel = 0) { const result = {}; const unprocessedProperties = [...node.properties]; while (unprocessedProperties.length > 0) { const propertyNode = unprocessedProperties.shift(); const key = propertyNode.keyNode.value; if (key === '<<' && propertyNode.valueNode) { // Handle merge key const mergeValue = this.resolveMergeValue(propertyNode.valueNode, doc, currentRecursionLevel + 1); if (mergeValue && typeof mergeValue === 'object' && !Array.isArray(mergeValue)) { // Merge properties from the resolved value const mergeKeys = Object.keys(mergeValue); for (const mergeKey of mergeKeys) { result[mergeKey] = mergeValue[mergeKey]; } } } else { // Regular property result[key] = this.astNodeToValue(propertyNode.valueNode, doc, currentRecursionLevel); } } return result; } /** * Resolves a merge value (which might be an alias) and recursively resolves its merge keys * @param node The AST node that might be an alias or object * @param doc The YAML document for resolving anchors * @param currentRecursionLevel Current recursion level * @returns The resolved value */ resolveMergeValue(node, doc, currentRecursionLevel) { const MAX_MERGE_RECURSION_LEVEL = 10; // Check if we've exceeded max recursion level if (currentRecursionLevel >= MAX_MERGE_RECURSION_LEVEL) { return { '<<': node.parent.internalNode['value'] + ' (recursion limit reached)' }; } // If it's an object node, resolve its merge keys if (node.type === 'object') { return this.resolveMergeKeys(node, doc, currentRecursionLevel); } // Otherwise, convert to value return this.astNodeToValue(node, doc, currentRecursionLevel); } /** * Converts an AST node to a plain JavaScript value * @param node The AST node to convert * @param doc The YAML document for resolving anchors * @param currentRecursionLevel Current recursion level * @returns The converted value */ astNodeToValue(node, doc, currentRecursionLevel) { if (!node) { return null; } switch (node.type) { case 'object': { return this.resolveMergeKeys(node, doc, currentRecursionLevel); } case 'array': { return node.children.map((child) => this.astNodeToValue(child, doc, currentRecursionLevel)); } case 'string': case 'number': case 'boolean': case 'null': { return node.value; } default: { return this.nodeToValue(node.internalNode); } } } /** * Converts a YAML Node to a plain JavaScript value * @param node The YAML node to convert * @returns The converted value */ nodeToValue(node) { if ((0, yaml_1.isAlias)(node)) { return node.source; } if ((0, yaml_1.isMap)(node)) { const result = {}; for (const pair of node.items) { if (pair.key && pair.value) { const key = this.nodeToValue(pair.key); const value = this.nodeToValue(pair.value); if (typeof key === 'string') { result[key] = value; } } } return result; } if ((0, yaml_1.isSeq)(node)) { return node.items.map((item) => this.nodeToValue(item)); } return node.value; } // copied from https://github.com/microsoft/vscode-json-languageservice/blob/2ea5ad3d2ffbbe40dea11cfe764a502becf113ce/src/services/jsonHover.ts#L112 toMarkdown(plain) { if (plain) { let escaped = plain.replace(/([^\n\r])(\r?\n)([^\n\r])/gm, '$1\n\n$3'); // single new lines to \n\n (Markdown paragraph) escaped = escaped.replace(/[\\`*_{}[\]#+!]/g, '\\$&'); // escape some of the markdown syntax tokens http://daringfireball.net/projects/markdown/syntax#backslash to avoid unintended formatting if (this.indentation !== undefined) { // escape indentation whitespace to prevent it from being converted to markdown code blocks. const indentationMatchRegex = new RegExp(` {${this.indentation.length}}`, 'g'); escaped = escaped.replace(indentationMatchRegex, '&emsp;'); } return escaped; } return undefined; } } exports.YAMLHover = YAMLHover; function ensureLineBreak(content) { if (content.length === 0) { return content; } if (!content.endsWith('\n')) { content += '\n'; } return content + '\n'; } function getSchemaName(schema) { let result = 'JSON Schema'; const urlString = schema.url; if (urlString) { const url = vscode_uri_1.URI.parse(urlString); result = path.basename(url.fsPath); } else if (schema.title) { result = schema.title; } return result; } // copied from https://github.com/microsoft/vscode-json-languageservice/blob/2ea5ad3d2ffbbe40dea11cfe764a502becf113ce/src/services/jsonHover.ts#L122 function toMarkdownCodeBlock(content) { // see https://daringfireball.net/projects/markdown/syntax#precode if (content.indexOf('`') !== -1) { return '`` ' + content + ' ``'; } return content; } /** * check all the schemas which is inside anyOf presented or not in matching schema. * @param node node * @param matchingSchemas all matching schema * @param schema scheam which is having anyOf * @returns true if all the schemas which inside anyOf presents in matching schema */ function isAllSchemasMatched(node, matchingSchemas, schema) { let count = 0; for (const matchSchema of matchingSchemas) { if (node === matchSchema.node && matchSchema.schema !== schema) { schema.anyOf.forEach((childSchema) => { if (matchSchema.schema.title === childSchema.title && matchSchema.schema.description === childSchema.description && matchSchema.schema.properties === childSchema.properties) { count++; } }); } } return count === schema.anyOf.length; } //# sourceMappingURL=yamlHover.js.map