UNPKG

@typespec/compiler

Version:

TypeSpec compiler and standard library

286 lines 11.3 kB
import { isDocument, isMap, isPair, isScalar, isSeq, parseDocument, visit, } from "yaml"; import { firstNonWhitespaceCharacterIndex, isWhitespaceStringOrUndefined } from "../utils/misc.js"; export function resolveYamlPositionDetail(document, position, log) { const pos = document.offsetAt(position); const content = document.getText(); const lines = content.split("\n"); const targetLine = lines[position.line]; const yamlDoc = parseDocument(content, { keepSourceTokens: true, }); if (!yamlDoc) { return undefined; } if (isCommentLine(targetLine) && position.character > firstNonWhitespaceCharacterIndex(targetLine)) { return undefined; } if (isWhitespaceStringOrUndefined(targetLine) || isCommentLine(targetLine)) { const indent = position.character; if (indent === 0) { const rootProperties = []; if (isMap(yamlDoc.contents)) { rootProperties.push(...yamlDoc.contents.items.map((item) => item.key.source ?? "")); } else if (isScalar(yamlDoc.contents) && !isWhitespaceStringOrUndefined(yamlDoc.contents.source)) { rootProperties.push(yamlDoc.contents.source); } return { path: [""], type: "key", source: "", sourceType: "PLAIN", siblings: rootProperties, yamlDoc, sourceRange: undefined, cursorPosition: pos, }; } for (let i = position.line - 1; i >= 0; i--) { const preLine = lines[i]; if (isWhitespaceStringOrUndefined(preLine) || isCommentLine(preLine)) { continue; } const preIndent = getIndentOfYamlLine(preLine); if (preIndent === -1) { // we got an empty '-' line return undefined; } if (preIndent === indent) { // we found the previous line which should be the sibling of the current line const found = findScalarNode(yamlDoc, document.offsetAt({ line: i, character: preIndent, })); if (!found) { log({ level: "debug", message: `Failed to find the scalar node in the found previous sibling line of empty line. curLine: ${targetLine}, preLine: ${preLine}`, }); return undefined; } const yp = createYamlPathFromVisitScalarNode(found, pos, log, yamlDoc); if (!yp || yp.path.length === 0) { log({ level: "debug", message: `Unexpected found path which should at least contains one segment for the found previous non-empty line item`, }); return undefined; } // adjust path and sibling for the whitespace line if (yp.type === "arr-item") { // the sibling node is a plain text value of an array (otherwise there should be an map node under the seq node) return undefined; } else { return { path: [...yp.path.slice(0, yp.path.length - 1), ""], type: "key", source: "", sourceType: "PLAIN", siblings: [...yp.siblings, yp.source], yamlDoc, sourceRange: yp.sourceRange, cursorPosition: pos, }; } break; } else if (preIndent < indent) { // we found the previous line which should be the parent of the current line const found = findScalarNode(yamlDoc, document.offsetAt({ line: i, character: preIndent, })); if (!found) { log({ level: "debug", message: `Failed to find the scalar node in the found previous parent line of empty line. curLine: ${targetLine}, preLine: ${preLine}`, }); return undefined; } // the parent should be a map or null (potential a map) const last = found.path[found.path.length - 1]; if (isPair(last) && (last.value === null || isMap(last.value) || (isScalar(last.value) && isWhitespaceStringOrUndefined(last.value.source)))) { const yp = createYamlPathFromVisitScalarNode(found, pos, log, yamlDoc); if (!yp || yp.path.length === 0) { log({ level: "debug", message: `Unexpected found path which should at least contains one segment for the found previous non-empty line item`, }); return undefined; } // adjust path and sibling for the whitespace line return { path: [...yp.path, ""], type: "key", source: "", sourceType: "PLAIN", siblings: isMap(last.value) ? (last.value?.items.map((item) => item.key.source ?? "") ?? []) : [], yamlDoc, sourceRange: yp.sourceRange, cursorPosition: pos, }; } break; } } return undefined; } else { const found = findScalarNode(yamlDoc, pos); if (!found) { log({ level: "debug", message: `Failed to find the scalar node at the position: ${pos}`, }); return undefined; } const yp = createYamlPathFromVisitScalarNode(found, pos, log, yamlDoc); return yp; } } function createYamlPathFromVisitScalarNode(info, offset, log, yamlDoc) { const { key, n, path: nodePath } = info; if (nodePath.length === 0) { log({ level: "debug", message: `Unexpected path structure, nodePath is empty while we always expect at least a root doc node`, }); return undefined; } const path = []; for (let i = 0; i < nodePath.length; i++) { const seg = nodePath[i]; if (isPair(seg)) { path.push(seg.key.source ?? ""); } else if (isSeq(seg)) { const next = nodePath.length > i + 1 ? nodePath[i + 1] : n; const index = seg.items.findIndex((item) => item === next); if (index >= 0) { path.push(index.toString()); } else { log({ level: "debug", message: `Unexpected path structure. the next element isn't found in the sequence(array): ${next.toJSON()}`, }); return undefined; } } } const last = nodePath[nodePath.length - 1]; let curSourceRange = undefined; if (n.range) { const [startPos, endPos] = n.range; curSourceRange = { pos: startPos, end: endPos }; } if (isDocument(last)) { // we are at the root and the content is a pure text (otherwise there should be a Map under document node first) return { path: [], type: key === null ? "key" : "value", source: n.source ?? "", sourceType: n.type ?? "PLAIN", siblings: [], yamlDoc, sourceRange: curSourceRange, cursorPosition: offset, }; } else if (isPair(last)) { if (nodePath.length < 2) { log({ level: "debug", message: `Unexpected path structure, the pair node should have a parent node: ${last.toString()}`, }); return undefined; } const newline = last.srcToken?.sep?.find((t) => t.type === "newline"); if (key === "value" && newline && newline.offset < offset) { // if the scalar node is marked as value but separated by newline from the key, it's more likely that the user is inputting the first property of an object // so build the target as an object key path.push(n.source ?? ""); return { path, type: "key", source: n.source ?? "", sourceType: n.type ?? "PLAIN", siblings: [], yamlDoc, sourceRange: curSourceRange, cursorPosition: offset, }; } else { const parent = nodePath.length >= 2 ? nodePath[nodePath.length - 2] : undefined; const targetSiblings = isMap(parent) ? parent.items.filter((item) => item !== last).map((item) => item.key.source ?? "") : []; return { path: path, type: key === "key" ? "key" : "value", source: n.source ?? "", siblings: targetSiblings, sourceType: n.type ?? "PLAIN", yamlDoc, sourceRange: curSourceRange, cursorPosition: offset, }; } } else if (isSeq(last)) { return { path: path, type: "arr-item", source: n.source ?? "", sourceType: n.type ?? "PLAIN", siblings: last.items .filter((i) => i !== n) .map((item) => (isScalar(item) ? (item.source ?? "") : "")), yamlDoc, sourceRange: curSourceRange, cursorPosition: offset, }; } else { log({ level: "debug", message: `Unexpected path structure, last element is not pair or seq for scalar node: ${last.toString()}`, }); return undefined; } } function isCommentLine(line) { return line.trimStart().startsWith("#"); } /** whitespace and '-' will be considered as indent */ function getIndentOfYamlLine(line) { return line.search(/[^\s-]/); } function findScalarNode(document, pos) { let found = undefined; visit(document, { Node: (key, n, path) => { if (isScalar(n)) { const [start, endValue, _endNode] = n.range ?? []; if (start === undefined || endValue === undefined) return undefined; if (start <= pos && pos <= endValue) { found = { key, n, path }; return visit.BREAK; } } return undefined; }, }); return found; } //# sourceMappingURL=yaml-resolver.js.map