UNPKG

@typespec/compiler

Version:

TypeSpec Compiler Preview

394 lines 16.4 kB
import { CompletionItemKind, MarkupKind, Range, TextEdit, } from "vscode-languageserver"; import { getSymNode } from "../core/binder.js"; import { getDeprecationDetails } from "../core/deprecation.js"; import { compilerAssert, getSourceLocation } from "../core/diagnostics.js"; import { printIdentifier } from "../core/helpers/syntax-utils.js"; import { getFirstAncestor, positionInRange } from "../core/parser.js"; import { getAnyExtensionFromPath, getBaseFileName, getDirectoryPath, hasTrailingDirectorySeparator, resolvePath, } from "../core/path-utils.js"; import { SyntaxKind, } from "../core/types.js"; import { findProjectRoot, loadFile } from "../utils/io.js"; import { resolveTspMain } from "../utils/misc.js"; import { getSymbolDetails } from "./type-details.js"; export async function resolveCompletion(context, posDetail) { let node = posDetail.node; if (!node) { if (posDetail.triviaStartPosition === 0 || !addCompletionByLookingBackward(posDetail, context)) { addKeywordCompletion("root", context.completions); } } else { // look back first to see whether we can get some completion from the previous statement, e.g. `model Foo |` if (!addCompletionByLookingBackward(posDetail, context)) { if (posDetail.inTrivia) { // If we're not immediately after an identifier character, then advance // the position past any trivia. This is done because a zero-width // inserted missing identifier that the user is now trying to complete // starts after the trivia following the cursor. node = posDetail.getPositionDetailAfterTrivia().node; } await AddCompletionNonTrivia(node, context, posDetail); } else { if (!posDetail.inTrivia) { await AddCompletionNonTrivia(node, context, posDetail); } } } return context.completions; } function addCompletionByLookingBackward(posDetail, context) { if (posDetail.triviaStartPosition === 0) { return false; } const preDetail = posDetail.getPositionDetailBeforeTrivia(); if (!preDetail.node) { return false; } const node = getFirstAncestor(preDetail.node, (n) => n.kind === SyntaxKind.ModelStatement || n.kind === SyntaxKind.ScalarStatement || n.kind === SyntaxKind.OperationStatement || n.kind === SyntaxKind.InterfaceStatement || n.kind === SyntaxKind.TemplateParameterDeclaration, true /*includeSelf*/); return node !== undefined && addCompletionByLookingBackwardNode(node, posDetail, context); } function addCompletionByLookingBackwardNode(preNode, posDetail, context) { const getIdentifierEndPos = (n) => { // n.pos === n.end, it means it's a missing identifier, just return -1; return n.pos === n.end ? -1 : n.end; }; const map = { [SyntaxKind.ModelStatement]: "modelHeader", [SyntaxKind.ScalarStatement]: "scalarHeader", [SyntaxKind.OperationStatement]: "operationHeader", [SyntaxKind.InterfaceStatement]: "interfaceHeader", }; switch (preNode?.kind) { case SyntaxKind.ModelStatement: case SyntaxKind.ScalarStatement: case SyntaxKind.OperationStatement: case SyntaxKind.InterfaceStatement: const idEndPos = preNode.templateParametersRange.end >= 0 ? preNode.templateParametersRange.end : getIdentifierEndPos(preNode.id); if (posDetail.triviaStartPosition === idEndPos) { const key = map[preNode.kind]; if (!key) { compilerAssert(false, "KeywordArea missing in keyarea map."); } addKeywordCompletion(key, context.completions); return true; } break; case SyntaxKind.TemplateParameterDeclaration: if (posDetail.triviaStartPosition === getIdentifierEndPos(preNode.id)) { addKeywordCompletion("templateParameter", context.completions); return true; } else if (preNode.parent?.templateParametersRange.end === posDetail.triviaStartPosition) { return addCompletionByLookingBackwardNode(preNode.parent, posDetail, context); } break; } return false; } async function AddCompletionNonTrivia(node, context, posDetail, lookBackward = true) { if (node === undefined || node.kind === SyntaxKind.InvalidStatement || (node.kind === SyntaxKind.Identifier && (node.parent?.kind === SyntaxKind.TypeSpecScript || node.parent?.kind === SyntaxKind.NamespaceStatement))) { addKeywordCompletion("root", context.completions); } else { switch (node.kind) { case SyntaxKind.NamespaceStatement: addKeywordCompletion("namespace", context.completions); break; case SyntaxKind.ScalarStatement: if (positionInRange(posDetail.position, node.bodyRange)) { addKeywordCompletion("scalarBody", context.completions); } break; case SyntaxKind.Identifier: addDirectiveCompletion(context, node); addIdentifierCompletion(context, node); break; case SyntaxKind.StringLiteral: if (node.parent && node.parent.kind === SyntaxKind.ImportStatement) { await addImportCompletion(context, node); } break; case SyntaxKind.ModelStatement: case SyntaxKind.ObjectLiteral: case SyntaxKind.ModelExpression: addModelCompletion(context, posDetail); break; } } } const keywords = [ // Root only ["import", { root: true }], // Root and namespace ["using", { root: true, namespace: true }], ["model", { root: true, namespace: true }], ["scalar", { root: true, namespace: true }], ["namespace", { root: true, namespace: true }], ["interface", { root: true, namespace: true }], ["union", { root: true, namespace: true }], ["enum", { root: true, namespace: true }], ["alias", { root: true, namespace: true }], ["op", { root: true, namespace: true }], ["dec", { root: true, namespace: true }], ["fn", { root: true, namespace: true }], ["const", { root: true, namespace: true }], // On model `model Foo <keyword> ...` [ "extends", { modelHeader: true, scalarHeader: true, templateParameter: true, interfaceHeader: true }, ], ["is", { modelHeader: true, operationHeader: true }], // On identifier ["true", { identifier: true }], ["false", { identifier: true }], ["unknown", { identifier: true }], ["void", { identifier: true }], ["never", { identifier: true }], // Modifiers ["extern", { root: true, namespace: true }], // Scalars ["init", { scalarBody: true }], ]; function addKeywordCompletion(area, completions) { const filteredKeywords = keywords.filter(([_, x]) => area in x); for (const [keyword] of filteredKeywords) { completions.items.push({ label: keyword, kind: CompletionItemKind.Keyword, }); } } async function loadPackageJson(host, path) { const [libPackageJson] = await loadFile(host, path, JSON.parse, () => { }); return libPackageJson; } /** Check if the folder given has a package.json which has a tspMain. */ async function isTspLibraryPackage(host, dir) { const libPackageJson = await loadPackageJson(host, resolvePath(dir, "package.json")); return resolveTspMain(libPackageJson) !== undefined; } async function addLibraryImportCompletion({ program, file, completions }, node) { const documentPath = file.file.path; const projectRoot = await findProjectRoot(program.host.stat, documentPath); if (projectRoot !== undefined) { const packagejson = await loadPackageJson(program.host, resolvePath(projectRoot, "package.json")); let dependencies = []; if (packagejson.dependencies !== undefined) { dependencies = dependencies.concat(Object.keys(packagejson.dependencies)); } if (packagejson.peerDependencies !== undefined) { dependencies = dependencies.concat(Object.keys(packagejson.peerDependencies)); } for (const dependency of dependencies) { const dependencyDir = resolvePath(projectRoot, "node_modules", dependency); if (await isTspLibraryPackage(program.host, dependencyDir)) { const range = { start: file.file.getLineAndCharacterOfPosition(node.pos + 1), end: file.file.getLineAndCharacterOfPosition(node.end - 1), }; completions.items.push({ textEdit: TextEdit.replace(range, dependency), label: dependency, kind: CompletionItemKind.Module, }); } } } } async function addImportCompletion(context, node) { if (node.value.startsWith("./") || node.value.startsWith("../")) { await addRelativePathCompletion(context, node); } else if (!node.value.startsWith(".")) { await addLibraryImportCompletion(context, node); } } async function tryListItemInDir(host, path) { try { return await host.readDir(path); } catch (e) { if (e.code === "ENOENT") { return []; } throw e; } } async function addRelativePathCompletion({ program, completions, file }, node) { const documentPath = file.file.path; const documentFile = getBaseFileName(documentPath); const documentDir = getDirectoryPath(documentPath); const currentRelativePath = hasTrailingDirectorySeparator(node.value) ? node.value : getDirectoryPath(node.value); const currentAbsolutePath = resolvePath(documentDir, currentRelativePath); const files = (await tryListItemInDir(program.host, currentAbsolutePath)).filter((x) => x !== documentFile && x !== "node_modules"); const lastSlash = node.value.lastIndexOf("/"); const offset = lastSlash === -1 ? 0 : lastSlash + 1; const range = { start: file.file.getLineAndCharacterOfPosition(node.pos + 1 + offset), end: file.file.getLineAndCharacterOfPosition(node.end - 1), }; for (const file of files) { const extension = getAnyExtensionFromPath(file); switch (extension) { case ".tsp": case ".js": case ".mjs": completions.items.push({ label: file, kind: CompletionItemKind.File, textEdit: TextEdit.replace(range, file), }); break; case "": completions.items.push({ label: file, kind: CompletionItemKind.Folder, textEdit: TextEdit.replace(range, file), }); break; } } } function addModelCompletion(context, posDetail) { const node = posDetail.node; if (!node || (node.kind !== SyntaxKind.ModelStatement && node.kind !== SyntaxKind.ModelExpression && node.kind !== SyntaxKind.ObjectLiteral)) { return; } if (posDetail.position === node.bodyRange.end) { // skip the scenario like `{ ... }|` return; } else { // create a fake identifier node to further resolve the completions for the model/object // it's a little tricky but can help to keep things clean and simple while the cons. is limited // TODO: consider adding support in resolveCompletions for non-identifier-node directly when we find more scenario and worth the cost const fakeProp = { kind: node.kind === SyntaxKind.ObjectLiteral ? SyntaxKind.ObjectLiteralProperty : SyntaxKind.ModelProperty, flags: 0 /* NodeFlags.None */, parent: node, }; const fakeId = { kind: SyntaxKind.Identifier, sv: "", flags: 0 /* NodeFlags.None */, parent: fakeProp, }; addIdentifierCompletion(context, fakeId); } } /** * Add completion options for an identifier. */ function addIdentifierCompletion({ program, completions }, node) { const result = program.checker.resolveCompletions(node); if (result.size === 0) { return; } for (const [key, { sym, label, suffix }] of result) { let kind; let deprecated = false; const symNode = getSymNode(sym); const type = sym.type ?? program.checker.getTypeForNode(symNode); if (sym.flags & (2048 /* SymbolFlags.Function */ | 512 /* SymbolFlags.Decorator */)) { kind = CompletionItemKind.Function; } else if (sym.flags & 256 /* SymbolFlags.Namespace */ && symNode.kind !== SyntaxKind.NamespaceStatement) { kind = CompletionItemKind.Module; } else if (symNode?.kind === SyntaxKind.AliasStatement) { kind = CompletionItemKind.Variable; deprecated = getDeprecationDetails(program, symNode) !== undefined; } else { kind = getCompletionItemKind(program, type); deprecated = getDeprecationDetails(program, type) !== undefined; } const documentation = getSymbolDetails(program, sym); const item = { label: label ?? key, documentation: documentation ? { kind: MarkupKind.Markdown, value: documentation, } : undefined, kind, }; if (sym.name.startsWith("$")) { const targetNode = getSourceLocation(node); const lineAndChar = targetNode.file.getLineAndCharacterOfPosition(node.pos); item.textEdit = TextEdit.replace( // Specifying replacement in the current location can avoid the problem of $ duplication Range.create(lineAndChar, lineAndChar), printIdentifier(key) + (suffix ?? "")); } else { item.insertText = printIdentifier(key) + (suffix ?? ""); } if (deprecated) { // hide these deprecated items to discourage the usage // not using CompletionItemTag.Deprecated because the strike-through is a little confusing // and also it's not supported in vs extension continue; } completions.items.push(item); } if (node.parent?.kind === SyntaxKind.TypeReference) { addKeywordCompletion("identifier", completions); } } const directiveNames = ["suppress", "deprecated"]; function addDirectiveCompletion({ completions }, node) { if (!(node.parent?.kind === SyntaxKind.DirectiveExpression && node.parent.target === node)) { return; } for (const directive of directiveNames) { completions.items.push({ label: directive, kind: CompletionItemKind.Keyword, }); } } function getCompletionItemKind(program, target) { switch (target.node?.kind) { case SyntaxKind.EnumStatement: case SyntaxKind.UnionStatement: return CompletionItemKind.Enum; case SyntaxKind.EnumMember: case SyntaxKind.UnionVariant: return CompletionItemKind.EnumMember; case SyntaxKind.AliasStatement: return CompletionItemKind.Variable; case SyntaxKind.ModelStatement: return CompletionItemKind.Class; case SyntaxKind.ScalarStatement: return CompletionItemKind.Unit; case SyntaxKind.ModelProperty: return CompletionItemKind.Field; case SyntaxKind.OperationStatement: return CompletionItemKind.Method; case SyntaxKind.NamespaceStatement: return CompletionItemKind.Module; default: return CompletionItemKind.Struct; } } //# sourceMappingURL=completion.js.map