258 lines
10 KiB
JavaScript
258 lines
10 KiB
JavaScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Red Hat, Inc. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
import { JSONDocument } from './jsonParser07';
|
|
import { isNode, isPair, isScalar, visit } from 'yaml';
|
|
import { defaultOptions, parse as parseYAML } from './yamlParser07';
|
|
import { ErrorCode } from 'vscode-json-languageservice';
|
|
import { convertAST } from './ast-converter';
|
|
import { isArrayEqual } from '../utils/arrUtils';
|
|
import { getParent } from '../utils/astUtils';
|
|
import { getIndentation } from '../utils/strings';
|
|
/**
|
|
* These documents are collected into a final YAMLDocument
|
|
* and passed to the `parseYAML` caller.
|
|
*/
|
|
export class SingleYAMLDocument extends JSONDocument {
|
|
constructor(lineCounter) {
|
|
super(null, []);
|
|
this.lineCounter = lineCounter;
|
|
}
|
|
/**
|
|
* Create a deep copy of this document
|
|
*/
|
|
clone() {
|
|
const copy = new SingleYAMLDocument(this.lineCounter);
|
|
copy.isKubernetes = this.isKubernetes;
|
|
copy.disableAdditionalProperties = this.disableAdditionalProperties;
|
|
copy.uri = this.uri;
|
|
copy.currentDocIndex = this.currentDocIndex;
|
|
copy._lineComments = this.lineComments.slice();
|
|
// this will re-create root node
|
|
copy.internalDocument = this._internalDocument.clone();
|
|
return copy;
|
|
}
|
|
collectLineComments() {
|
|
this._lineComments = [];
|
|
if (this._internalDocument.commentBefore) {
|
|
const comments = this._internalDocument.commentBefore.split('\n');
|
|
comments.forEach((comment) => this._lineComments.push(`#${comment}`));
|
|
}
|
|
visit(this.internalDocument, (_key, node) => {
|
|
if (node?.commentBefore) {
|
|
const comments = node?.commentBefore.split('\n');
|
|
comments.forEach((comment) => this._lineComments.push(`#${comment}`));
|
|
}
|
|
if (node?.comment) {
|
|
this._lineComments.push(`#${node.comment}`);
|
|
}
|
|
});
|
|
if (this._internalDocument.comment) {
|
|
this._lineComments.push(`#${this._internalDocument.comment}`);
|
|
}
|
|
}
|
|
/**
|
|
* Updates the internal AST tree of the object
|
|
* from the internal node. This is call whenever the
|
|
* internalDocument is set but also can be called to
|
|
* reflect any changes on the underlying document
|
|
* without setting the internalDocument explicitly.
|
|
*/
|
|
updateFromInternalDocument() {
|
|
this.root = convertAST(null, this._internalDocument.contents, this._internalDocument, this.lineCounter);
|
|
}
|
|
set internalDocument(document) {
|
|
this._internalDocument = document;
|
|
this.updateFromInternalDocument();
|
|
}
|
|
get internalDocument() {
|
|
return this._internalDocument;
|
|
}
|
|
get lineComments() {
|
|
if (!this._lineComments) {
|
|
this.collectLineComments();
|
|
}
|
|
return this._lineComments;
|
|
}
|
|
set lineComments(val) {
|
|
this._lineComments = val;
|
|
}
|
|
get errors() {
|
|
return this.internalDocument.errors.map(YAMLErrorToYamlDocDiagnostics);
|
|
}
|
|
get warnings() {
|
|
return this.internalDocument.warnings.map(YAMLErrorToYamlDocDiagnostics);
|
|
}
|
|
getNodeFromPosition(positionOffset, textBuffer, configuredIndentation) {
|
|
const position = textBuffer.getPosition(positionOffset);
|
|
const lineContent = textBuffer.getLineContent(position.line);
|
|
if (lineContent.trim().length === 0) {
|
|
return [this.findClosestNode(positionOffset, textBuffer, configuredIndentation), true];
|
|
}
|
|
const textAfterPosition = lineContent.substring(position.character);
|
|
const spacesAfterPositionMatch = textAfterPosition.match(/^([ ]+)\n?$/);
|
|
const areOnlySpacesAfterPosition = !!spacesAfterPositionMatch;
|
|
const countOfSpacesAfterPosition = spacesAfterPositionMatch?.[1].length;
|
|
let closestNode;
|
|
visit(this.internalDocument, (key, node) => {
|
|
if (!node) {
|
|
return;
|
|
}
|
|
const range = node.range;
|
|
if (!range) {
|
|
return;
|
|
}
|
|
const isNullNodeOnTheLine = () => areOnlySpacesAfterPosition &&
|
|
positionOffset + countOfSpacesAfterPosition === range[2] &&
|
|
isScalar(node) &&
|
|
node.value === null;
|
|
if ((range[0] <= positionOffset && range[1] >= positionOffset) || isNullNodeOnTheLine()) {
|
|
closestNode = node;
|
|
}
|
|
else {
|
|
return visit.SKIP;
|
|
}
|
|
});
|
|
return [closestNode, false];
|
|
}
|
|
findClosestNode(offset, textBuffer, configuredIndentation) {
|
|
let offsetDiff = this.internalDocument.range[2];
|
|
let maxOffset = this.internalDocument.range[0];
|
|
let closestNode;
|
|
visit(this.internalDocument, (key, node) => {
|
|
if (!node) {
|
|
return;
|
|
}
|
|
const range = node.range;
|
|
if (!range) {
|
|
return;
|
|
}
|
|
const diff = range[1] - offset;
|
|
if (maxOffset <= range[0] && diff <= 0 && Math.abs(diff) <= offsetDiff) {
|
|
offsetDiff = Math.abs(diff);
|
|
maxOffset = range[0];
|
|
closestNode = node;
|
|
}
|
|
});
|
|
const position = textBuffer.getPosition(offset);
|
|
const lineContent = textBuffer.getLineContent(position.line);
|
|
const indentation = getIndentation(lineContent, position.character);
|
|
if (isScalar(closestNode) && closestNode.value === null) {
|
|
return closestNode;
|
|
}
|
|
if (indentation === position.character) {
|
|
closestNode = this.getProperParentByIndentation(indentation, closestNode, textBuffer, '', configuredIndentation);
|
|
}
|
|
return closestNode;
|
|
}
|
|
getProperParentByIndentation(indentation, node, textBuffer, currentLine, configuredIndentation, rootParent) {
|
|
if (!node) {
|
|
return this.internalDocument.contents;
|
|
}
|
|
configuredIndentation = !configuredIndentation ? 2 : configuredIndentation;
|
|
if (isNode(node) && node.range) {
|
|
const position = textBuffer.getPosition(node.range[0]);
|
|
const lineContent = textBuffer.getLineContent(position.line);
|
|
currentLine = currentLine === '' ? lineContent.trim() : currentLine;
|
|
if (currentLine.startsWith('-') && indentation === configuredIndentation && currentLine === lineContent.trim()) {
|
|
position.character += indentation;
|
|
}
|
|
if (position.character > indentation && position.character > 0) {
|
|
const parent = this.getParent(node);
|
|
if (parent) {
|
|
return this.getProperParentByIndentation(indentation, parent, textBuffer, currentLine, configuredIndentation, rootParent);
|
|
}
|
|
}
|
|
else if (position.character < indentation) {
|
|
const parent = this.getParent(node);
|
|
if (isPair(parent) && isNode(parent.value)) {
|
|
return parent.value;
|
|
}
|
|
else if (isPair(rootParent) && isNode(rootParent.value)) {
|
|
return rootParent.value;
|
|
}
|
|
}
|
|
else {
|
|
return node;
|
|
}
|
|
}
|
|
else if (isPair(node)) {
|
|
rootParent = node;
|
|
const parent = this.getParent(node);
|
|
return this.getProperParentByIndentation(indentation, parent, textBuffer, currentLine, configuredIndentation, rootParent);
|
|
}
|
|
return node;
|
|
}
|
|
getParent(node) {
|
|
return getParent(this.internalDocument, node);
|
|
}
|
|
}
|
|
/**
|
|
* Contains the SingleYAMLDocuments, to be passed
|
|
* to the `parseYAML` caller.
|
|
*/
|
|
export class YAMLDocument {
|
|
constructor(documents, tokens) {
|
|
this.documents = documents;
|
|
this.tokens = tokens;
|
|
this.errors = [];
|
|
this.warnings = [];
|
|
}
|
|
}
|
|
export class YamlDocuments {
|
|
constructor() {
|
|
// a mapping of URIs to cached documents
|
|
this.cache = new Map();
|
|
}
|
|
/**
|
|
* Get cached YAMLDocument
|
|
* @param document TextDocument to parse
|
|
* @param parserOptions YAML parserOptions
|
|
* @param addRootObject if true and document is empty add empty object {} to force schema usage
|
|
* @returns the YAMLDocument
|
|
*/
|
|
getYamlDocument(document, parserOptions, addRootObject = false) {
|
|
this.ensureCache(document, parserOptions ?? defaultOptions, addRootObject);
|
|
return this.cache.get(document.uri).document;
|
|
}
|
|
/**
|
|
* For test purpose only!
|
|
*/
|
|
clear() {
|
|
this.cache.clear();
|
|
}
|
|
ensureCache(document, parserOptions, addRootObject) {
|
|
const key = document.uri;
|
|
if (!this.cache.has(key)) {
|
|
this.cache.set(key, { version: -1, document: new YAMLDocument([], []), parserOptions: defaultOptions });
|
|
}
|
|
const cacheEntry = this.cache.get(key);
|
|
if (cacheEntry.version !== document.version ||
|
|
(parserOptions.customTags && !isArrayEqual(cacheEntry.parserOptions.customTags, parserOptions.customTags))) {
|
|
let text = document.getText();
|
|
// if text is contains only whitespace wrap all text in object to force schema selection
|
|
if (addRootObject && !/\S/.test(text)) {
|
|
text = `{${text}}`;
|
|
}
|
|
const doc = parseYAML(text, parserOptions, document);
|
|
cacheEntry.document = doc;
|
|
cacheEntry.version = document.version;
|
|
cacheEntry.parserOptions = parserOptions;
|
|
}
|
|
}
|
|
}
|
|
export const yamlDocumentsCache = new YamlDocuments();
|
|
function YAMLErrorToYamlDocDiagnostics(error) {
|
|
return {
|
|
message: error.message,
|
|
location: {
|
|
start: error.pos[0],
|
|
end: error.pos[1],
|
|
toLineEnd: true,
|
|
},
|
|
severity: 1,
|
|
code: ErrorCode.Undefined,
|
|
};
|
|
}
|
|
//# sourceMappingURL=yaml-documents.js.map
|