UNPKG

@typespec/compiler

Version:

TypeSpec compiler and standard library

403 lines 19.3 kB
import { CompletionItemKind, Position, Range, TextEdit, } from "vscode-languageserver"; import { isMap, isPair } from "yaml"; import { emitterOptionsSchema, TypeSpecConfigJsonSchema } from "../../config/config-schema.js"; import { compilerFeatureNames, compilerFeatures } from "../../core/features.js"; import { getDirectoryPath, getNormalizedAbsolutePath, joinPaths, normalizeSlashes, } from "../../core/path-utils.js"; import { distinctArray } from "../../utils/misc.js"; import { resolveYamlPositionDetail } from "../yaml-resolver.js"; export async function provideTspconfigCompletionItems(tspConfigDoc, tspConfigPosition, context) { const { fileService, compilerHost, emitterProvider, linterProvider, log } = context; const target = resolveYamlPositionDetail(tspConfigDoc, tspConfigPosition, log); if (target === undefined) { return []; } // Variable interpolation if (target.sourceRange) { const variableInterpolationItems = resolveVariableInterpolationCompleteItems(target.yamlDoc, target.path, tspConfigDoc.getText().slice(target.sourceRange.pos, target.cursorPosition)); if (variableInterpolationItems.length > 0) { return variableInterpolationItems; } } const items = resolveTspConfigCompleteItems(await fileService.getPath(tspConfigDoc), target, tspConfigPosition, log); return items; async function resolveTspConfigCompleteItems(tspConfigFile, target, tspConfigPosition, log) { const { path: nodePath, type: targetType, siblings, source } = target; const CONFIG_PATH_LENGTH_FOR_EMITTER_LIST = 2; const CONFIG_PATH_LENGTH_FOR_LINTER_LIST = 2; const CONFIG_PATH_LENGTH_FOR_EXTENDS = 1; const CONFIG_PATH_LENGTH_FOR_IMPORTS = 2; const CONFIG_PATH_LENGTH_FOR_FEATURES = 2; if ((nodePath.length === CONFIG_PATH_LENGTH_FOR_EMITTER_LIST && nodePath[0] === "options" && targetType === "key") || (nodePath.length === CONFIG_PATH_LENGTH_FOR_EMITTER_LIST && nodePath[0] === "emit" && targetType === "arr-item")) { const emitters = await emitterProvider.listLibraries(tspConfigFile); const items = []; for (const [name, pkg] of Object.entries(emitters)) { if (!siblings.includes(name)) { const item = createCompletionItemWithQuote(name, (await pkg.getPackageJsonData())?.description ?? `Emitter from ${name}`, tspConfigPosition, target); items.push(item); } } return items; } else if (nodePath.length > CONFIG_PATH_LENGTH_FOR_EMITTER_LIST && nodePath[0] === "options") { const emitterName = nodePath[CONFIG_PATH_LENGTH_FOR_EMITTER_LIST - 1]; const emitter = await emitterProvider.getLibrary(tspConfigFile, emitterName); if (!emitter) { return []; } const exports = await emitter.getModuleExports(); const builtInEmitterSchema = emitterOptionsSchema; const itemsFromBuiltIn = builtInEmitterSchema ? resolveCompleteItems(builtInEmitterSchema, { ...target, path: nodePath.slice(CONFIG_PATH_LENGTH_FOR_EMITTER_LIST), }) : []; const itemsFromEmitter = []; if (exports?.$lib?.emitter?.options !== undefined) { const more = resolveCompleteItems(exports.$lib.emitter.options, { ...target, path: nodePath.slice(CONFIG_PATH_LENGTH_FOR_EMITTER_LIST), }); itemsFromEmitter.push(...more); } return [...itemsFromBuiltIn, ...itemsFromEmitter]; } else if (nodePath.length > CONFIG_PATH_LENGTH_FOR_LINTER_LIST && nodePath[0] === "linter") { const extendKeyWord = nodePath[CONFIG_PATH_LENGTH_FOR_LINTER_LIST - 1]; const items = []; const linters = await linterProvider.listLibraries(tspConfigFile); if (extendKeyWord === "extends") { for (const [name, pkg] of Object.entries(linters)) { // If a ruleSet exists for the linter, add it to the end of the library name. const exports = await pkg.getModuleExports(); if (exports?.$linter?.ruleSets !== undefined) { // Below ruleSets are objects rather than arrays for (const [ruleSet] of Object.entries(exports?.$linter?.ruleSets)) { const labelName = `${name}/${ruleSet}`; if (!siblings.includes(labelName)) { const item = createCompletionItemWithQuote(labelName, (await pkg.getPackageJsonData())?.description ?? `Linters from ${labelName}`, tspConfigPosition, target); items.push(item); } } } // Add the library name directly. if (!siblings.includes(name)) { const item = createCompletionItemWithQuote(name, (await pkg.getPackageJsonData())?.description ?? `Linters from ${name}`, tspConfigPosition, target); items.push(item); } } } else if (extendKeyWord === "enable" || extendKeyWord === "disable") { // enable/disable rules for (const [name, pkg] of Object.entries(linters)) { const exports = await pkg.getModuleExports(); if (exports?.$linter?.rules !== undefined) { for (const [, rule] of Object.entries(exports?.$linter?.rules)) { const labelName = `${name}/${rule.name}`; const item = createCompletionItemWithQuote(labelName, rule.description, tspConfigPosition, target); items.push(item); } } } } else { log({ level: "warning", message: "Unknown linter keyword, it should be 'extends', 'enable' or 'disable'", }); } return items; } else if (nodePath.length === CONFIG_PATH_LENGTH_FOR_FEATURES && nodePath[0] === "features" && targetType === "arr-item") { return compilerFeatureNames .filter((name) => !siblings.includes(name)) .map((name) => createCompletionItemWithQuote(name, compilerFeatures[name].description, tspConfigPosition, target)); } else if (nodePath.length === CONFIG_PATH_LENGTH_FOR_EXTENDS && nodePath[0] === "extends") { const currentFolder = getDirectoryPath(tspConfigFile); const newFolderPath = joinPaths(currentFolder, source); // Exclude the current yaml configuration file const relativeFiles = await findFilesOrDirsWithSameExtension(compilerHost, newFolderPath, ".yaml", [normalizeSlashes(tspConfigFile)]); return getFilePathCompletionItems(relativeFiles, siblings, source); } else if (nodePath.length >= CONFIG_PATH_LENGTH_FOR_IMPORTS && nodePath[0] === "imports") { const currentFolder = getDirectoryPath(tspConfigFile); const newFolderPath = joinPaths(currentFolder, source); // Exclude main.tsp files that are the same as the current yaml configuration directory const relativeFiles = await findFilesOrDirsWithSameExtension(compilerHost, newFolderPath, ".tsp", [joinPaths(currentFolder, "main.tsp")]); return getFilePathCompletionItems(relativeFiles, siblings, source); } else { const schema = TypeSpecConfigJsonSchema; return schema ? resolveCompleteItems(schema, target) : []; } } /** * * @param schema * @param path * @param curIndex * @param track track the schema we have processed so that we won't have problem for circle reference * @returns */ function findSchemaByPath(schema, path, curIndex, track = new Set()) { if (curIndex >= path.length) { return [schema]; } if (track.has(schema)) { return []; } const key = path[curIndex]; const result = []; if (schema.type === "array") { return findSchemaByPath(schema.items, path, curIndex + 1); } else { if (schema.type === "object") { if (schema.properties && schema.properties[key]) { if (schema.properties[key].deprecated !== true) { const found = findSchemaByPath(schema.properties[key], path, curIndex + 1); result.push(...found); } } else if (schema.additionalProperties && typeof schema.additionalProperties === "object") { const found = findSchemaByPath(schema.additionalProperties, path, curIndex + 1); result.push(...found); } } if (schema.oneOf || schema.anyOf || schema.allOf) { const choices = [...(schema.anyOf ?? []), ...(schema.oneOf ?? []), ...(schema.allOf ?? [])]; const founds = choices.flatMap((choice) => findSchemaByPath(choice, path, curIndex), track); result.push(...founds); } return result; } } /** * * @param schema * @param track a set to track all the processed schema so that we won't have problem for circle reference * @returns */ function expandPossibleSchema(schema, track = new Set()) { if (track.has(schema)) return []; const result = [schema]; const choices = [...(schema.anyOf ?? []), ...(schema.oneOf ?? []), ...(schema.allOf ?? [])]; for (const choice of choices) { result.push(...expandPossibleSchema(choice, track)); } result.forEach((s) => track.add(s)); return result; } function resolveCompleteItems(schema, target) { const { path: nodePath, type: targetType } = target; // if the target is a key which means it's pointing to an object property, we should remove the last element of the path to get it's parent object for its schema const path = targetType === "key" ? nodePath.slice(0, -1) : nodePath; const foundSchemas = findSchemaByPath(schema, path, 0); const result = []; foundSchemas .flatMap((s) => expandPossibleSchema(s)) .forEach((cur) => { if (targetType === "key" || targetType === "arr-item") { if (cur.type === "object") { const props = Object.keys(cur.properties ?? {}) .filter((key) => !target.siblings.includes(key) && cur.properties[key].deprecated !== true) .map((key) => { const item = { label: key, kind: CompletionItemKind.Field, documentation: cur.required?.includes(key) ? "[required]\n" + (cur.properties[key].description ?? "") : "[optional]\n" + (cur.properties[key].description ?? ""), }; return item; }); result.push(...props); } } if (targetType === "value" || targetType === "arr-item") { if (cur.type === "boolean") { result.push(...["true", "false"].map((value) => { const item = { label: value, kind: CompletionItemKind.Value, documentation: cur.description, }; return item; })); } else if (cur.type === "string" && cur.enum) { const enums = cur.enum.map((value) => { const item = { label: value, kind: CompletionItemKind.Value, documentation: cur.description, insertText: `"${value}"`, }; return item; }); result.push(...enums); } } }); return distinctArray(result, (t) => t.label); } } /** * Create the common CompletionItem object, which value is included in quotes * @param labelName The value of the label attribute of the CompletionItem object * @param description The value of the documentation attribute of the CompletionItem object * @param tspConfigPosition Input current line location object, see {@link Position} * @param target The target object of the current configuration file, see {@link YamlPositionDetail} * @returns CompletionItem object */ function createCompletionItemWithQuote(labelName, description, tspConfigPosition, target) { if (target.sourceRange && target.cursorPosition >= target.sourceRange.pos && target.cursorPosition <= target.sourceRange.end) { // If it is a quoted string, the relative position needs to be reduced by 1 const lenRelativeToStartPos = target.sourceType === "QUOTE_SINGLE" || target.sourceType === "QUOTE_DOUBLE" ? target.cursorPosition - target.sourceRange.pos - 1 : target.cursorPosition - target.sourceRange.pos; return { label: labelName, kind: CompletionItemKind.Field, documentation: description, textEdit: TextEdit.replace(Range.create(Position.create(tspConfigPosition.line, tspConfigPosition.character - lenRelativeToStartPos), Position.create(tspConfigPosition.line, tspConfigPosition.character)), target.sourceType === "QUOTE_SINGLE" || target.sourceType === "QUOTE_DOUBLE" ? labelName : `"${labelName}"`), }; } return { label: labelName, kind: CompletionItemKind.Field, documentation: description, insertText: `"${labelName}"`, }; } /** * Find a set of dirs/files with the same suffix * @param compilerHost CompilerHost object for file operations, see {@link CompilerHost} * @param rootPath The absolute input path or relative path of the current configuration file * @param fileExtension File extension * @param excludeFiles exclude files array, default is [] * @returns dirs/files array */ async function findFilesOrDirsWithSameExtension(compilerHost, rootPath, fileExtension, excludeFiles = []) { const exclude = ["node_modules", "tsp-output", ".vs", ".vscode"]; const files = []; try { // When reading the content under the path, an error may be reported if the path is incorrect. const dirs = await compilerHost.readDir(rootPath); for (const d of dirs) { if (!exclude.includes(d) && !excludeFiles.includes(getNormalizedAbsolutePath(d, rootPath))) { try { const stat = await compilerHost.stat(joinPaths(rootPath, d)); if (stat.isDirectory() || (stat.isFile() && d.endsWith(fileExtension))) { files.push(d); } } catch { // If the path is incorrect, the error is ignored continue; } } } } catch { return files; } return files; } /** * Get the CompletionItem object array of the relative path of the file * @param relativeFiles File relative path array * @param siblings Sibling node array * @param source The input source of the current node * @returns CompletionItem object array */ function getFilePathCompletionItems(relativeFiles, siblings, source) { return relativeFiles .filter((file) => !siblings.includes(joinPaths(source, file))) .map((file) => { return { label: file, kind: CompletionItemKind.File, }; }); } /** * resolve variable interpolation completion items * @param yamlDocNodes tsp config yaml document nodes , see {@link Document<Node, true>} * @param path current path of the target node, such as ["linter", "extends","- ┆"] * @param curText current editing text, from startPos to curPos * @returns variable interpolation completion items */ function resolveVariableInterpolationCompleteItems(yamlDocNodes, path, curText) { if (/{\s*env\.[^}]*$/.test(curText)) { // environment-variables return getVariableCompletionItem(yamlDocNodes, "environment-variables", "Custom environment variables"); } else if (/{[^}]*$/.test(curText)) { // parameters and built-in variables const result = [ ...getVariableCompletionItem(yamlDocNodes, "parameters", "Custom paramters variables"), ...["cwd", "project-root"].map((value) => { const item = { label: value, kind: CompletionItemKind.Value, documentation: "Built-in variables", }; return item; }), ]; // if the current path is options, add output-dir and emitter-name const CONFIG_PATH_LENGTH_FOR_OPTIONS = 2; if (path.length > CONFIG_PATH_LENGTH_FOR_OPTIONS && path[0] === "options") { result.push(...["output-dir", "emitter-name"].map((value) => { const item = { label: value, kind: CompletionItemKind.Value, documentation: "Built-in variables", }; return item; })); } return result; } return []; } /** * Get the corresponding CompletionItem array based on the filter name * @param yamlDoc The contents of the YAML configuration file * @param filterName The filter name * @param description The description of the CompletionItem object * @returns CompletionItem object array */ function getVariableCompletionItem(yamlDoc, filterName, description) { const result = []; if (isMap(yamlDoc.contents)) { const yamlMap = yamlDoc.contents.items.find((item) => item.key.source === filterName); if (yamlMap && yamlMap.value !== null && isMap(yamlMap.value)) { yamlMap.value.items.forEach((i) => { if (isPair(i)) { result.push({ label: i.key.source ?? "", kind: CompletionItemKind.Value, documentation: description, }); } }); } } return result; } //# sourceMappingURL=completion.js.map