UNPKG

@microsoft/compose-language-service

Version:
332 lines 19.1 kB
"use strict"; /*!-------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var _ComposeDocument_textDocument; Object.defineProperty(exports, "__esModule", { value: true }); exports.KeyValueRegex = exports.ComposeDocument = void 0; const vscode_languageserver_1 = require("vscode-languageserver"); const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument"); const yaml_1 = require("yaml"); const DocumentSettings_1 = require("../client/DocumentSettings"); const ActionContext_1 = require("./utils/ActionContext"); const Lazy_1 = require("./utils/Lazy"); // The stated behavior of character number in the `Position` class is to roll back to line length if it exceeds the line length. So, this will work for any line <1m characters. That should cover most of them. const MaximumLineLength = 1000 * 1000; class ComposeDocument { get textDocument() { return __classPrivateFieldGet(this, _ComposeDocument_textDocument, "f"); } get id() { return { uri: this.textDocument.uri, }; } get uri() { return this.textDocument.uri; } constructor(doc) { this.yamlDocument = new Lazy_1.Lazy(() => this.buildYamlDocument()); /* private */ _ComposeDocument_textDocument.set(this, void 0); __classPrivateFieldSet(this, _ComposeDocument_textDocument, doc, "f"); } update(doc) { __classPrivateFieldSet(this, _ComposeDocument_textDocument, doc, "f"); this.yamlDocument.clear(); return this; } /** * Gets the text of a line at a given line number or position, including the line break (`\n` or `\r\n`) at the end if present * @param line The line number or `Position` * @returns The line text */ lineAt(line) { // Flatten to a position at the start of the line const startOfLine = (typeof line === 'number') ? vscode_languageserver_1.Position.create(line, 0) : vscode_languageserver_1.Position.create(line.line, 0); const endOfLine = vscode_languageserver_1.Position.create(startOfLine.line, MaximumLineLength); if (startOfLine.line > this.textDocument.lineCount) { throw new Error(`Requested line ${startOfLine.line} is out of bounds.`); } return this.textDocument.getText(vscode_languageserver_1.Range.create(startOfLine, endOfLine)); } /** * Gets settings from the document. If already populated, that will be returned. If supported, the info will be requested from the client. * Otherwise, the settings will be heuristically guessed based on document contents. * Note: The client is also directed to notify the server if the settings change, via the `DocumentSettingsNotification` notification. * @returns The document settings (tab size, line endings, etc.) */ async getSettings() { var _a, _b, _c; // First, try asking the client, if the capability is present if (!this.documentSettings) { const ctx = (0, ActionContext_1.getCurrentContext)(); if ((_c = (_b = (_a = ctx.clientCapabilities) === null || _a === void 0 ? void 0 : _a.experimental) === null || _b === void 0 ? void 0 : _b.documentSettings) === null || _c === void 0 ? void 0 : _c.request) { const result = await ctx.connection.sendRequest(DocumentSettings_1.DocumentSettingsRequest.type, { textDocument: this.id }); if (result) { this.documentSettings = result; } } } // If the capability is not present, or the above didn't get a result for some reason, try heuristically guessing if (!this.documentSettings) { this.documentSettings = this.guessDocumentSettings(); } return this.documentSettings; } /** * Updates the settings (tab size, line endings, etc.) for the document. This is meant to be called by the server upon receiving a `DocumentSettingsNotification`. * @param params The new settings for the document */ updateSettings(params) { this.documentSettings = params; } /** * Gets information about the position, including the tab depth and a logical path describing where in the YAML tree the position is * @param params The `ExtendedPositionParams` for the position being queried * @returns A `PositionInfo` object with tab depth and logical path * @example If the position is in a service foo's `image` key, the logical path would be `/services/foo/image`. */ async getPositionInfo(params) { const { tabSize } = await this.getSettings(); const partialPositionInfo = this.getFirstLinePositionInfo(params, tabSize); const fullPathParts = [...partialPositionInfo.pathParts]; // We no longer need to consider position, since the cursor is inherently below this point // So now we just scroll upward, ignoring any lines with equal or more indentation than the first line (where the cursor is) let currentIndentDepth = partialPositionInfo.cursorIndentDepth; for (let i = params.position.line - 1; i >= 0 && currentIndentDepth > 0; i--) { const currentLine = this.lineAt(i); let indentDepth = MaximumLineLength; let result; /* eslint-disable @typescript-eslint/no-non-null-assertion */ if ((result = ItemValueRegex.exec(currentLine))) { indentDepth = result.groups['indent'].length / tabSize; if (indentDepth < currentIndentDepth) { // If this line is an ItemValue and less indented, then add `<item>` to the path currentIndentDepth = indentDepth; fullPathParts.unshift(Item); } } else if ((result = exports.KeyValueRegex.exec(currentLine))) { indentDepth = result.groups['indent'].length / tabSize; if (indentDepth < currentIndentDepth || (indentDepth === currentIndentDepth && fullPathParts[0] === Item)) // YAML is too permissive and allows for items to have the same indentation as their parent key, so need to account for that { // If this line is a KeyValue (which also includes keys alone) and less indented, add that key to the path currentIndentDepth = indentDepth; fullPathParts.unshift(result.groups['keyName']); } } /* eslint-enable @typescript-eslint/no-non-null-assertion */ } return { path: '/' + fullPathParts.join('/'), indentDepth: partialPositionInfo.cursorIndentDepth, // Indent depth is determined entirely from `getFirstLinePositionInfo` }; } buildYamlDocument() { const yamlDocument = (0, yaml_1.parseDocument)(this.textDocument.getText(), { merge: true, prettyErrors: true, customTags: ComposeCustomTags }); if (!(0, yaml_1.isDocument)(yamlDocument)) { throw new vscode_languageserver_1.ResponseError(vscode_languageserver_1.ErrorCodes.ParseError, 'Malformed YAML document'); } return yamlDocument; } guessDocumentSettings() { var _a; const documentText = this.textDocument.getText(); // For line endings, see if there are any \r const eol = /\r/ig.test(documentText) ? DocumentSettings_1.CRLF : DocumentSettings_1.LF; // For tab size, look for the first key with nonzero indentation. If none found, assume 2. let tabSize = 2; const indentedKeyLineMatch = /^(?<indentation>[ ]+)(?<key>[.\w-]+:\s*)$/im.exec(documentText); if ((_a = indentedKeyLineMatch === null || indentedKeyLineMatch === void 0 ? void 0 : indentedKeyLineMatch.groups) === null || _a === void 0 ? void 0 : _a['indentation']) { tabSize = indentedKeyLineMatch.groups['indentation'].length; } return { eol, tabSize, }; } /** * This method is responsible for determining the part of the logical path for the line where the cursor is, given the * cursor's position. * This is similar to the body of `getPositionInfo` but differs in a key way--the position is on this line, so the * character position can fundamentally affect what the logical path is * @param params Parameters including position * @param tabSize The tab size (needed to determine indent depth) * @returns The part(s) of the logical path at the current line */ getFirstLinePositionInfo(params, tabSize) { const currentLine = this.lineAt(params.position); const pathParts = []; let cursorIndentDepth = 0; let result; /* eslint-disable @typescript-eslint/no-non-null-assertion */ if ((result = LineWithCommentRegex.exec(currentLine))) { const commentSepPosition = currentLine.indexOf(result.groups['commentSep']); // If the cursor is in a comment, then it's not "in" the document per se, but in the comment // So the path will only be "/<comment>", and the cursorIndentDepth is set to -1 to short-circuit the loop above if (params.position.character > commentSepPosition) { return { pathParts: [Comment], cursorIndentDepth: -1, }; } } if ((result = ItemKeyValueRegex.exec(currentLine))) { // First, see if it's an ItemKeyValue, i.e. ` - foo: bar` const itemSepPosition = currentLine.indexOf(result.groups['itemInd']); const keySepPosition = currentLine.indexOf(result.groups['keyInd'], itemSepPosition); const indentLength = result.groups['indent'].length; const keyName = result.groups['keyName']; cursorIndentDepth = indentLength / tabSize; if (params.position.character > keySepPosition) { // If the position is after the key separator, we're in the value pathParts.unshift(Value); pathParts.unshift(keyName); pathParts.unshift(Item); } else if (params.position.character === keySepPosition) { // If the position is at the key separator, we're on the separator pathParts.unshift(Sep); pathParts.unshift(keyName); pathParts.unshift(Item); } else if (params.position.character > itemSepPosition) { // Otherwise if the position is after the item separator, we're in the key pathParts.unshift(keyName); pathParts.unshift(Item); } else if (params.position.character === itemSepPosition) { // If we're at the item separator, we're at the item separator (of course) pathParts.unshift(Sep); pathParts.unshift(Item); } else if (params.position.character < indentLength) { // Otherwise if we're somewhere within the indentation, we're not at a "position" within this line, but we do need to consider the indent depth cursorIndentDepth = params.position.character / tabSize; } } else if ((result = exports.KeyValueRegex.exec(currentLine))) { // Next, check if it's a standard KeyValue, i.e. ` foo: bar` const keySepPosition = currentLine.indexOf(result.groups['keyInd']); const indentLength = result.groups['indent'].length; const keyName = result.groups['keyName']; cursorIndentDepth = indentLength / tabSize; if (params.position.character > keySepPosition) { // If the position is after the key separator, we're in the value pathParts.unshift(Value); pathParts.unshift(keyName); } else if (params.position.character === keySepPosition) { // If the position is at the key separator, we're on the separator pathParts.unshift(Sep); pathParts.unshift(keyName); } else if (params.position.character >= indentLength) { // If the position is after the indent, we're in the key pathParts.unshift(keyName); } else if (params.position.character < indentLength) { // Otherwise if we're somewhere within the indentation, we're not at a "position" within this line, but we do need to consider the indent depth cursorIndentDepth = params.position.character / tabSize; } } else if ((result = ItemValueRegex.exec(currentLine))) { // Next, check if it's an ItemValue, i.e. ` - foo` const itemSepPosition = currentLine.indexOf(result.groups['itemInd']); const indentLength = result.groups['indent'].length; cursorIndentDepth = indentLength / tabSize; if (params.position.character > itemSepPosition) { // If the position is after the item separator, we're in the value pathParts.unshift(Value); pathParts.unshift(Item); } else if (params.position.character === itemSepPosition) { // If we're at the item separator, we're at the item separator (of course) pathParts.unshift(Sep); pathParts.unshift(Item); } else if (params.position.character < indentLength) { // Otherwise if we're somewhere within the indentation, we're not at a "position" within this line, but we do need to consider the indent depth cursorIndentDepth = params.position.character / tabSize; } } else if ((result = ValueRegex.exec(currentLine))) { // Next, check if it's a value alone, i.e. ` foo` const indentLength = result.groups['indent'].length; cursorIndentDepth = indentLength / tabSize; if (params.position.character >= indentLength) { // If the position is after the indent, we're in the value pathParts.unshift(Value); } else if (params.position.character < indentLength) { // Otherwise if we're somewhere within the indentation, we're not at a "position" within this line, but we do need to consider the indent depth cursorIndentDepth = params.position.character / tabSize; } } else if ((result = WhitespaceRegex.exec(currentLine))) { // Last, check if it's whitespace only const indentLength = result.groups['indent'].length; cursorIndentDepth = indentLength / tabSize; if (params.position.character < indentLength) { // Simply need to determine the indent depth cursorIndentDepth = params.position.character / tabSize; } } /* eslint-enable @typescript-eslint/no-non-null-assertion */ return { pathParts, cursorIndentDepth, }; } } _ComposeDocument_textDocument = new WeakMap(); ComposeDocument.DocumentManagerConfig = { create: (uri, languageId, version, content) => new ComposeDocument(vscode_languageserver_textdocument_1.TextDocument.create(uri, languageId, version, content)), update: (document, changes, version) => document.update(vscode_languageserver_textdocument_1.TextDocument.update(document.textDocument, changes, version)), }; exports.ComposeDocument = ComposeDocument; // IMPORTANT: For all of these regular expressions, the groups present *and* their names are very important. // Removal or alteration would break `getPositionInfo` and `getFirstLinePositionInfo` // A regex for matching a standard key/value line, i.e. `key: value` // Exported for use by KeyHoverProvider exports.KeyValueRegex = /^(?<indent> *)(?<key>(?<keyName>[.\w-]+)(?<keyInd>(?<keySep>:) *))(?<value>.*)$/im; // A regex for matching an item/value line, i.e. `- value` const ItemValueRegex = /^(?<indent> *)(?<itemInd>(?<itemSep>-) *)(?<value>.*)$/im; // A regex for matching an item/key/value line, i.e. `- key: value`. This will be the top line of a flow map. const ItemKeyValueRegex = /^(?<indent> *)(?<itemInd>(?<itemSep>-) *)(?<key>(?<keyName>[.\w-]+)(?<keyInd>(?<keySep>:) *))(?<value>.*)$/im; // A regex for matching any value line const ValueRegex = /^(?<indent> *)(?<value>\S+)$/im; // A regex for matching a whitespace-only line const WhitespaceRegex = /^(?<indent> *)$/im; // A regex for matching any line with a comment (or if the whole line is a comment) // TODO: This will match a 'string with a #', which is *not* a comment, but currently this will not affect any end scenarios // TODO: The closest I've gotten is /(['"])?.*(?<commentSep>#)(?:(?!\1).)*$/im const LineWithCommentRegex = /^.*(?<commentSep>#).*$/im; // Constants for marking non-key parts of a logical path const Value = '<value>'; const Item = '<item>'; const Sep = '<sep>'; const Comment = '<comment>'; // Custom YAML tags that Compose supports const ComposeCustomTags = [ { tag: '!reset', collection: 'seq', resolve: v => v }, { tag: '!reset', collection: 'map', resolve: v => v }, { tag: '!reset', resolve: v => v }, { tag: '!override', collection: 'seq', resolve: v => v }, { tag: '!override', collection: 'map', resolve: v => v }, { tag: '!override', resolve: v => v }, ]; //# sourceMappingURL=ComposeDocument.js.map