@typespec/compiler
Version:
TypeSpec Compiler Preview
1,166 lines (1,165 loc) • 50.4 kB
JavaScript
import { CodeAction, CodeActionKind, CompletionList, DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, FileChangeType, MarkupKind, Range, SemanticTokensBuilder, TextDocumentSyncKind, TextEdit, } from "vscode-languageserver/node.js";
import { getSymNode } from "../core/binder.js";
import { resolveCodeFix } from "../core/code-fixes.js";
import { compilerAssert, getSourceLocation } from "../core/diagnostics.js";
import { formatTypeSpec } from "../core/formatter.js";
import { getEntityName, getTypeName } from "../core/helpers/type-name-utils.js";
import { builtInLinterRule_UnusedTemplateParameter } from "../core/linter-rules/unused-template-parameter.rule.js";
import { builtInLinterRule_UnusedUsing } from "../core/linter-rules/unused-using.rule.js";
import { builtInLinterLibraryName } from "../core/linter.js";
import { formatLog } from "../core/logger/index.js";
import { getPositionBeforeTrivia } from "../core/parser-utils.js";
import { getNodeAtPosition, getNodeAtPositionDetail, visitChildren } from "../core/parser.js";
import { ensureTrailingDirectorySeparator, getDirectoryPath, joinPaths, normalizePath, resolvePath, } from "../core/path-utils.js";
import { skipTrivia, skipWhiteSpace } from "../core/scanner.js";
import { createSourceFile, getSourceFileKindFromExt } from "../core/source-file.js";
import { NoTarget, SyntaxKind, } from "../core/types.js";
import { getTypeSpecCoreTemplates } from "../init/core-templates.js";
import { validateTemplateDefinitions } from "../init/init-template-validate.js";
import { scaffoldNewProject } from "../init/scaffold.js";
import { typespecVersion } from "../manifest.js";
import { resolveModule } from "../module-resolver/module-resolver.js";
import { listAllFilesInDir } from "../utils/fs-utils.js";
import { getNormalizedRealPath, resolveTspMain } from "../utils/misc.js";
import { getSemanticTokens } from "./classify.js";
import { createCompileService } from "./compile-service.js";
import { resolveCompletion } from "./completion.js";
import { Commands } from "./constants.js";
import { convertDiagnosticToLsp } from "./diagnostics.js";
import { createFileService } from "./file-service.js";
import { createFileSystemCache } from "./file-system-cache.js";
import { LibraryProvider } from "./lib-provider.js";
import { NpmPackageProvider } from "./npm-package-provider.js";
import { getRenameImportEdit, getUpdatedImportValue } from "./rename-file.js";
import { getSymbolStructure } from "./symbol-structure.js";
import { provideTspconfigCompletionItems } from "./tspconfig/completion.js";
import { getParameterDocumentation, getSymbolDetails, getTemplateParameterDocumentation, } from "./type-details.js";
import { SemanticTokenKind, } from "./types.js";
export function createServer(host) {
const fileService = createFileService({ serverHost: host });
// Cache all file I/O. Only open documents are sent over the LSP pipe. When
// the compiler reads a file that isn't open, we use this cache to avoid
// hitting the disk. Entries are invalidated when LSP client notifies us of
// a file change.
const fileSystemCache = createFileSystemCache({
fileService,
log,
});
const compilerHost = createCompilerHost();
const npmPackageProvider = new NpmPackageProvider(compilerHost);
const emitterProvider = new LibraryProvider(npmPackageProvider, (exports) => exports.$onEmit !== undefined);
const linterProvider = new LibraryProvider(npmPackageProvider, (exports) => exports.$linter !== undefined);
const compileService = createCompileService({
fileService,
fileSystemCache,
compilerHost,
serverHost: host,
log,
});
const currentDiagnosticIndex = new Map();
let diagnosticIdCounter = 0;
compileService.on("compileEnd", (result) => reportDiagnostics(result));
let workspaceFolders = [];
let isInitialized = false;
let pendingMessages = [];
return {
get pendingMessages() {
return pendingMessages;
},
get workspaceFolders() {
return workspaceFolders;
},
compile: compileService.compile,
initialize,
initialized,
workspaceFoldersChanged,
watchedFilesChanged,
formatDocument,
gotoDefinition,
documentClosed,
complete,
findReferences,
findDocumentHighlight,
prepareRename,
rename,
renameFiles,
getSemanticTokens: getSemanticTokensForDocument,
buildSemanticTokens,
checkChange,
getFoldingRanges,
getHover,
getSignatureHelp,
getDocumentSymbols,
getCodeActions,
executeCommand,
log,
getInitProjectContext,
validateInitProjectTemplate,
initProject,
internalCompile,
};
async function initialize(params) {
const tokenLegend = {
tokenTypes: Object.keys(SemanticTokenKind)
.filter((x) => Number.isNaN(Number(x)))
.map((x) => x.slice(0, 1).toLocaleLowerCase() + x.slice(1)),
tokenModifiers: [],
};
const capabilities = {
textDocumentSync: TextDocumentSyncKind.Incremental,
definitionProvider: true,
foldingRangeProvider: true,
hoverProvider: true,
documentSymbolProvider: true,
documentHighlightProvider: true,
completionProvider: {
resolveProvider: false,
triggerCharacters: [".", "@", "/"],
},
semanticTokensProvider: {
full: true,
legend: tokenLegend,
},
referencesProvider: true,
renameProvider: {
prepareProvider: true,
},
documentFormattingProvider: true,
signatureHelpProvider: {
triggerCharacters: ["(", ",", "<"],
retriggerCharacters: [")"],
},
codeActionProvider: {
codeActionKinds: ["quickfix"],
},
executeCommandProvider: {
commands: [Commands.APPLY_CODE_FIX],
},
};
if (params.capabilities.workspace?.workspaceFolders) {
for (const w of params.workspaceFolders ?? []) {
workspaceFolders.push({
...w,
path: ensureTrailingDirectorySeparator(await fileService.fileURLToRealPath(w.uri)),
});
}
capabilities.workspace = {
workspaceFolders: {
supported: true,
changeNotifications: true,
},
fileOperations: {
didRename: {
filters: [
{
scheme: "file",
pattern: { glob: "**/*.tsp", matches: "file" },
},
{
scheme: "file",
pattern: { glob: "**/*", matches: "folder" },
},
],
},
},
};
// eslint-disable-next-line @typescript-eslint/no-deprecated
}
else if (params.rootUri) {
workspaceFolders = [
{
name: "<root>",
// eslint-disable-next-line @typescript-eslint/no-deprecated
uri: params.rootUri,
path: ensureTrailingDirectorySeparator(
// eslint-disable-next-line @typescript-eslint/no-deprecated
await fileService.fileURLToRealPath(params.rootUri)),
},
];
// eslint-disable-next-line @typescript-eslint/no-deprecated
}
else if (params.rootPath) {
workspaceFolders = [
{
name: "<root>",
// eslint-disable-next-line @typescript-eslint/no-deprecated
uri: compilerHost.pathToFileURL(params.rootPath),
path: ensureTrailingDirectorySeparator(
// eslint-disable-next-line @typescript-eslint/no-deprecated
await getNormalizedRealPath(compilerHost, params.rootPath)),
},
];
}
log({ level: "info", message: `Workspace Folders`, detail: workspaceFolders });
const customCapacities = {
getInitProjectContext: true,
initProject: true,
validateInitProjectTemplate: true,
internalCompile: true,
};
// the file path is expected to be .../@typespec/compiler/dist/src/server/serverlib.js
const curFile = normalizePath(compilerHost.fileURLToPath(import.meta.url));
const SERVERLIB_PATH_ENDWITH = "/dist/src/server/serverlib.js";
let compilerRootFolder = undefined;
if (!curFile.endsWith(SERVERLIB_PATH_ENDWITH)) {
log({ level: "warning", message: `Unexpected path for serverlib found: ${curFile}` });
}
else {
compilerRootFolder = curFile.slice(0, curFile.length - SERVERLIB_PATH_ENDWITH.length);
}
const result = {
serverInfo: {
name: "TypeSpec Language Server",
version: typespecVersion,
},
capabilities,
customCapacities,
compilerRootFolder,
compilerCliJsPath: compilerRootFolder
? joinPaths(compilerRootFolder, "cmd", "tsp.js")
: undefined,
};
return result;
}
function initialized(params) {
isInitialized = true;
log({ level: "info", message: "Initialization complete." });
}
async function getInitProjectContext() {
return {
coreInitTemplates: await getTypeSpecCoreTemplates(host.compilerHost),
};
}
async function validateInitProjectTemplate(param) {
const { template } = param;
// even when the strict validation fails, we still try to proceed with relaxed validation
// so just do relaxed validation directly here
const validationResult = validateTemplateDefinitions(template, NoTarget, false);
if (!validationResult.valid) {
for (const diag of validationResult.diagnostics) {
log({
level: diag.severity,
message: diag.message,
detail: {
code: diag.code,
url: diag.url,
},
});
}
}
return validationResult.valid;
}
async function initProject(param) {
try {
await scaffoldNewProject(compilerHost, param.config);
return true;
}
catch (e) {
log({ level: "error", message: "Unexpected error when initializing project", detail: e });
return false;
}
}
async function internalCompile(param) {
const option = {
...param.options,
};
const result = await compileService.compile(param.doc, option, {
bypassCache: true,
trackAction: true,
});
if (result === undefined) {
return {
hasError: true,
diagnostics: [
{
code: "internal-error",
message: "Failed to get compiler result, please check the compilation output for details",
severity: "error",
target: NoTarget,
url: undefined,
},
],
entrypoint: undefined,
options: undefined,
};
}
else {
return {
hasError: result.program.hasError(),
diagnostics: result.program.diagnostics.map((diagnostic) => {
const target = getSourceLocation(diagnostic.target, { locateId: true });
let position = undefined;
if (target?.file) {
const lineAndCharacter = target.file.getLineAndCharacterOfPosition(target.pos);
position = {
line: lineAndCharacter.line + 1,
column: lineAndCharacter.character + 1,
};
}
return {
code: diagnostic.code,
message: diagnostic.message,
severity: diagnostic.severity,
target: { ...target, position: position },
url: diagnostic.url,
};
}),
entrypoint: result.document?.uri,
options: result.program.compilerOptions,
};
}
}
async function renameFiles(params) {
const firstFilePath = params.files[0];
if (!firstFilePath) {
return;
}
// Update cache for renamed folders.
for (const file of params.files) {
const oldFilePath = await fileService.getPath({ uri: file.oldUri });
const newFilePath = await fileService.getPath({ uri: file.newUri });
const isDirRename = !file.oldUri.endsWith(".tsp");
if (isDirRename) {
const files = await listAllFilesInDir(compilerHost, newFilePath);
for (const file of files) {
const oldFile = resolvePath(oldFilePath, file);
fileSystemCache.notify([
{ uri: fileService.getURL(oldFile), type: FileChangeType.Deleted },
]);
}
}
}
const mainFile = await compileService.getMainFileForDocument(await fileService.getPath({ uri: firstFilePath.newUri }));
// Add this method to resolve timing issues between renamed files and `fs.stat`
// to prevent `fs.stat` from getting the files before modification.
// Currently the test requires a delay of 300ms
await new Promise((resolve) => setTimeout(resolve, 300));
const result = await compileService.compile({ uri: fileService.getURL(mainFile) });
if (!result) {
log({
level: "debug",
message: `The main tsp file '${mainFile}' compilation failed, please check the detailed compilation message`,
});
return;
}
for (const file of params.files) {
const oldFilePath = await fileService.getPath({ uri: file.oldUri });
const newFilePath = await fileService.getPath({ uri: file.newUri });
const isDirRename = !file.oldUri.endsWith(".tsp");
for (const diagnostic of result.program.diagnostics) {
if (diagnostic.code !== "import-not-found")
continue;
const target = diagnostic.target;
if (target.kind === SyntaxKind.ImportStatement) {
const result = getUpdatedImportValue(target, {
oldPath: oldFilePath,
newPath: newFilePath,
isDirRename,
});
if (result) {
log({
level: "info",
message: `The imports content '${target.path.value}' needs to be modified in tsp file '${result.filePath}' to '${result.newValue}'`,
});
const edit = getRenameImportEdit(target, result.newValue);
await host.applyEdit({ changes: { [fileService.getURL(result.filePath)]: [edit] } });
}
}
}
}
}
async function workspaceFoldersChanged(e) {
log({ level: "info", message: "Workspace Folders Changed", detail: e });
const map = new Map(workspaceFolders.map((f) => [f.uri, f]));
for (const folder of e.removed) {
map.delete(folder.uri);
}
for (const folder of e.added) {
map.set(folder.uri, {
...folder,
path: ensureTrailingDirectorySeparator(await fileService.fileURLToRealPath(folder.uri)),
});
}
workspaceFolders = Array.from(map.values());
log({ level: "info", message: `Workspace Folders`, detail: workspaceFolders });
}
function watchedFilesChanged(params) {
fileSystemCache.notify(params.changes);
npmPackageProvider.notify(params.changes);
}
function isTspConfigFile(doc) {
return doc.uri.endsWith("tspconfig.yaml");
}
async function getFoldingRanges(params) {
if (isTspConfigFile(params.textDocument))
return [];
const ast = await compileService.getScript(params.textDocument);
if (!ast) {
return [];
}
const file = ast.file;
const ranges = [];
let rangeStartSingleLines = -1;
for (let i = 0; i < ast.comments.length; i++) {
const comment = ast.comments[i];
if (comment.kind === SyntaxKind.LineComment &&
i + 1 < ast.comments.length &&
ast.comments[i + 1].kind === SyntaxKind.LineComment &&
ast.comments[i + 1].pos === skipWhiteSpace(file.text, comment.end)) {
if (rangeStartSingleLines === -1) {
rangeStartSingleLines = comment.pos;
}
}
else if (rangeStartSingleLines !== -1) {
addRange(rangeStartSingleLines, comment.end);
rangeStartSingleLines = -1;
}
else {
addRange(comment.pos, comment.end);
}
}
visitChildren(ast, addRangesForNode);
function addRangesForNode(node) {
if (node.kind === SyntaxKind.Doc) {
return; // fold doc comments as regular comments
}
let nodeStart = node.pos;
if ("decorators" in node && node.decorators.length > 0) {
const decoratorEnd = node.decorators[node.decorators.length - 1].end;
addRange(nodeStart, decoratorEnd);
nodeStart = skipTrivia(file.text, decoratorEnd);
}
addRange(nodeStart, node.end);
visitChildren(node, addRangesForNode);
}
return ranges;
function addRange(startPos, endPos) {
const start = file.getLineAndCharacterOfPosition(startPos);
const end = file.getLineAndCharacterOfPosition(endPos);
if (start.line !== end.line) {
ranges.push({
startLine: start.line,
startCharacter: start.character,
endLine: end.line,
endCharacter: end.character,
});
}
}
}
async function getDocumentSymbols(params) {
if (isTspConfigFile(params.textDocument))
return [];
const ast = await compileService.getScript(params.textDocument);
if (!ast) {
return [];
}
return getSymbolStructure(ast);
}
async function findDocumentHighlight(params) {
if (isTspConfigFile(params.textDocument))
return [];
const result = await compileService.compile(params.textDocument);
if (result === undefined) {
return [];
}
const { program, document, script } = result;
if (!document) {
return [];
}
const identifiers = findReferenceIdentifiers(program, script, document.offsetAt(params.position), [script]);
return identifiers.map((identifier) => ({
range: getRange(identifier, script.file),
kind: DocumentHighlightKind.Read,
}));
}
async function checkChange(change) {
if (isTspConfigFile(change.document))
return undefined;
compileService.notifyChange(change.document);
}
async function reportDiagnostics({ program, document, optionsFromConfig }) {
if (!document)
return undefined;
if (isTspConfigFile(document))
return undefined;
currentDiagnosticIndex.clear();
// Group diagnostics by file.
//
// Initialize diagnostics for all source files in program to empty array
// as we must send an empty array when a file has no diagnostics or else
// stale diagnostics from a previous run will stick around in the IDE.
//
const diagnosticMap = new Map();
diagnosticMap.set(document, []);
for (const each of program.sourceFiles.values()) {
const document = each.file?.document;
if (document) {
diagnosticMap.set(document, []);
}
}
for (const each of program.diagnostics) {
const results = convertDiagnosticToLsp(fileService, program, document, each);
for (const result of results) {
const [diagnostic, diagDocument] = result;
if (each.url) {
diagnostic.codeDescription = {
href: each.url,
};
}
const unusedUsingRule = `${builtInLinterLibraryName}/${builtInLinterRule_UnusedUsing}`;
const unusedTemlateParameterRule = `${builtInLinterLibraryName}/${builtInLinterRule_UnusedTemplateParameter}`;
if (each.code === "deprecated") {
diagnostic.tags = [DiagnosticTag.Deprecated];
}
else if (each.code === unusedUsingRule) {
// Unused or unnecessary code. Diagnostics with this tag are rendered faded out, so no extra work needed from IDE side
// https://vscode-api.js.org/enums/vscode.DiagnosticTag.html#google_vignette
// https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.languageserver.protocol.diagnostictag?view=visualstudiosdk-2022
diagnostic.tags = [DiagnosticTag.Unnecessary];
if (optionsFromConfig.linterRuleSet?.enable?.[unusedUsingRule] === undefined &&
optionsFromConfig.linterRuleSet?.disable?.[unusedUsingRule] === undefined) {
// if the unused using is not configured by user explicitly, report it as hint by default
diagnostic.severity = DiagnosticSeverity.Hint;
}
}
else if (each.code === unusedTemlateParameterRule) {
// Unused or unnecessary code. Diagnostics with this tag are rendered faded out, so no extra work needed from IDE side
// https://vscode-api.js.org/enums/vscode.DiagnosticTag.html#google_vignette
// https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.languageserver.protocol.diagnostictag?view=visualstudiosdk-2022
diagnostic.tags = [DiagnosticTag.Unnecessary];
if (optionsFromConfig.linterRuleSet?.enable?.[unusedTemlateParameterRule] === undefined &&
optionsFromConfig.linterRuleSet?.disable?.[unusedTemlateParameterRule] === undefined) {
// if the unused template parameter is not configured by user explicitly, report it as hint by default
diagnostic.severity = DiagnosticSeverity.Hint;
}
}
diagnostic.data = {
id: diagnosticIdCounter++,
file: diagDocument.uri,
};
const diagnostics = diagnosticMap.get(diagDocument);
compilerAssert(diagnostics, "Diagnostic reported against a source file that was not added to the program.");
diagnostics.push(diagnostic);
currentDiagnosticIndex.set(diagnostic.data.id, each);
}
}
for (const [document, diagnostics] of diagnosticMap) {
sendDiagnostics(document, diagnostics);
}
}
async function getHover(params) {
if (isTspConfigFile(params.textDocument))
return { contents: [] };
const result = await compileService.compile(params.textDocument);
if (result === undefined) {
return { contents: [] };
}
const { program, document, script } = result;
if (!document) {
return { contents: [] };
}
const id = getNodeAtPosition(script, document.offsetAt(params.position));
const sym = id?.kind === SyntaxKind.Identifier ? program.checker.resolveRelatedSymbols(id) : undefined;
if (!sym || sym.length === 0) {
return { contents: { kind: MarkupKind.Markdown, value: "" } };
}
else {
// Only show full definition if the symbol is a model or interface that has extends or is clauses.
// Avoid showing full definition in other cases which can be long and not useful
let includeExpandedDefinition = false;
const sn = getSymNode(sym[0]);
if (sn.kind !== SyntaxKind.AliasStatement) {
const type = sym[0].type ?? program.checker.getTypeOrValueForNode(sn);
if (type && "kind" in type) {
const modelHasExtendOrIs = type.kind === "Model" &&
(type.baseModel !== undefined ||
type.sourceModel !== undefined ||
type.sourceModels.length > 0);
const interfaceHasExtend = type.kind === "Interface" && type.sourceInterfaces.length > 0;
includeExpandedDefinition = modelHasExtendOrIs || interfaceHasExtend;
}
}
const markdown = {
kind: MarkupKind.Markdown,
value: sym && sym.length > 0
? getSymbolDetails(program, sym[0], {
includeSignature: true,
includeParameterTags: true,
includeExpandedDefinition,
})
: "",
};
return {
contents: markdown,
};
}
}
async function getSignatureHelp(params) {
if (isTspConfigFile(params.textDocument))
return undefined;
const result = await compileService.compile(params.textDocument);
if (result === undefined) {
return undefined;
}
const { script, document, program } = result;
if (!document) {
return undefined;
}
const data = getSignatureHelpNodeAtPosition(script, document.offsetAt(params.position));
if (data === undefined) {
return undefined;
}
const { node, argumentIndex } = data;
switch (node.kind) {
case SyntaxKind.TypeReference:
return getSignatureHelpForTemplate(program, node, argumentIndex);
case SyntaxKind.DecoratorExpression:
case SyntaxKind.AugmentDecoratorStatement:
return getSignatureHelpForDecorator(program, node, argumentIndex);
default:
const _assertNever = node;
compilerAssert(false, "Unreachable");
}
}
function getSignatureHelpForTemplate(program, node, argumentIndex) {
const sym = program.checker.resolveRelatedSymbols(node.target.kind === SyntaxKind.MemberExpression ? node.target.id : node.target);
if (!sym || sym.length <= 0) {
return undefined;
}
const templateDeclNode = sym[0].declarations[0];
if (!templateDeclNode ||
!("templateParameters" in templateDeclNode) ||
templateDeclNode.templateParameters.length === 0) {
return undefined;
}
const parameterDocs = getTemplateParameterDocumentation(templateDeclNode);
const parameters = templateDeclNode.templateParameters.map((x) => {
const info = { label: x.id.sv };
const doc = parameterDocs.get(x.id.sv);
if (doc) {
info.documentation = { kind: MarkupKind.Markdown, value: doc };
}
return info;
});
const help = {
signatures: [
{
label: `${sym[0].name}<${parameters.map((x) => x.label).join(", ")}>`,
parameters,
activeParameter: Math.min(parameters.length - 1, argumentIndex),
},
],
activeSignature: 0,
activeParameter: 0,
};
const doc = getSymbolDetails(program, sym[0], {
includeSignature: false,
includeParameterTags: false,
});
if (doc) {
help.signatures[0].documentation = { kind: MarkupKind.Markdown, value: doc };
}
return help;
}
function getSignatureHelpForDecorator(program, node, argumentIndex) {
const sym = program.checker.resolveRelatedSymbols(node.target.kind === SyntaxKind.MemberExpression ? node.target.id : node.target);
if (!sym || sym.length <= 0) {
return undefined;
}
const decoratorDeclNode = sym[0].declarations.find((x) => x.kind === SyntaxKind.DecoratorDeclarationStatement);
if (decoratorDeclNode === undefined) {
return undefined;
}
const type = program.checker.getTypeForNode(decoratorDeclNode);
compilerAssert(type.kind === "Decorator", "Expected type to be a decorator.");
const parameterDocs = getParameterDocumentation(program, type);
let labelPrefix = "";
const parameters = [];
if (node.kind === SyntaxKind.AugmentDecoratorStatement) {
const targetType = decoratorDeclNode.target.type
? program.checker.getTypeForNode(decoratorDeclNode.target.type)
: undefined;
parameters.push({
label: `${decoratorDeclNode.target.id.sv}: ${targetType ? getTypeName(targetType) : "unknown"}`,
});
labelPrefix = "@";
}
parameters.push(...type.parameters.map((x) => {
const info = {
// prettier-ignore
label: `${x.rest ? "..." : ""}${x.name}${x.optional ? "?" : ""}: ${getEntityName(x.type)}`,
};
const doc = parameterDocs.get(x.name);
if (doc) {
info.documentation = { kind: MarkupKind.Markdown, value: doc };
}
return info;
}));
const help = {
signatures: [
{
label: `${labelPrefix}${type.name}(${parameters.map((x) => x.label).join(", ")})`,
parameters,
activeParameter: Math.min(parameters.length - 1, argumentIndex),
},
],
activeSignature: 0,
activeParameter: 0,
};
const doc = getSymbolDetails(program, sym[0], {
includeSignature: false,
includeParameterTags: false,
});
if (doc) {
help.signatures[0].documentation = { kind: MarkupKind.Markdown, value: doc };
}
return help;
}
async function formatDocument(params) {
if (isTspConfigFile(params.textDocument))
return [];
const document = host.getOpenDocumentByURL(params.textDocument.uri);
if (document === undefined) {
return [];
}
const path = await fileService.fileURLToRealPath(params.textDocument.uri);
const prettierConfig = await resolvePrettierConfig(path);
const resolvedConfig = prettierConfig ?? {
tabWidth: params.options.tabSize,
useTabs: !params.options.insertSpaces,
};
log({
level: "info",
message: `Formatting TypeSpec document: ${JSON.stringify({ fileUri: params.textDocument.uri, vscodeOptions: params.options, prettierConfig, resolvedConfig }, null, 2)}`,
});
const formattedText = await formatTypeSpec(document.getText(), resolvedConfig);
return [minimalEdit(document, formattedText)];
}
async function resolvePrettierConfig(path) {
try {
// Resolve prettier if it is installed.
const prettier = await import("prettier");
return prettier.resolveConfig(path);
}
catch (e) {
return null;
}
}
function minimalEdit(document, string1) {
const string0 = document.getText();
// length of common prefix
let i = 0;
while (i < string0.length && i < string1.length && string0[i] === string1[i]) {
++i;
}
// length of common suffix
let j = 0;
while (i + j < string0.length &&
i + j < string1.length &&
string0[string0.length - j - 1] === string1[string1.length - j - 1]) {
++j;
}
const newText = string1.substring(i, string1.length - j);
const pos0 = document.positionAt(i);
const pos1 = document.positionAt(string0.length - j);
return TextEdit.replace(Range.create(pos0, pos1), newText);
}
async function gotoDefinition(params) {
if (isTspConfigFile(params.textDocument))
return [];
const result = await compileService.compile(params.textDocument);
if (result === undefined || result.document === undefined) {
return [];
}
const node = getNodeAtPosition(result.script, result.document.offsetAt(params.position));
switch (node?.kind) {
case SyntaxKind.Identifier:
const sym = result.program.checker.resolveRelatedSymbols(node);
return getLocations(sym && sym.length > 0 ? sym[0].declarations : undefined);
case SyntaxKind.StringLiteral:
if (node.parent?.kind === SyntaxKind.ImportStatement) {
return [await getImportLocation(node.value, result.script)];
}
else {
return [];
}
}
return [];
}
async function getImportLocation(importPath, currentFile) {
const host = {
realpath: compilerHost.realpath,
readFile: async (path) => {
const file = await compilerHost.readFile(path);
return file.text;
},
stat: compilerHost.stat,
};
const resolved = await resolveModule(host, importPath, {
baseDir: getDirectoryPath(currentFile.file.path),
resolveMain(pkg) {
// this lets us follow node resolve semantics more-or-less exactly
// but using tspMain instead of main.
return resolveTspMain(pkg) ?? pkg.main;
},
});
return {
uri: fileService.getURL(resolved.type === "file" ? resolved.path : resolved.mainFile),
range: Range.create(0, 0, 0, 0),
};
}
async function complete(params) {
if (isTspConfigFile(params.textDocument)) {
const doc = host.getOpenDocumentByURL(params.textDocument.uri);
if (doc) {
const items = await provideTspconfigCompletionItems(doc, params.position, {
fileService,
compilerHost,
emitterProvider,
linterProvider,
log,
});
return CompletionList.create(items);
}
return CompletionList.create([]);
}
const completions = {
isIncomplete: false,
items: [],
};
const result = await compileService.compile(params.textDocument);
if (result) {
const { script, document, program } = result;
if (!document) {
return completions;
}
const posDetail = getCompletionNodeAtPosition(script, document.offsetAt(params.position));
return await resolveCompletion({
program,
file: script,
completions,
params,
}, posDetail);
}
return completions;
}
async function findReferences(params) {
if (isTspConfigFile(params.textDocument))
return [];
const result = await compileService.compile(params.textDocument);
if (result === undefined || result.document === undefined) {
return [];
}
const identifiers = findReferenceIdentifiers(result.program, result.script, result.document.offsetAt(params.position));
return getLocations(identifiers);
}
async function prepareRename(params) {
if (isTspConfigFile(params.textDocument))
return undefined;
const result = await compileService.compile(params.textDocument);
if (result === undefined || result.document === undefined) {
return undefined;
}
const id = getNodeAtPosition(result.script, result.document.offsetAt(params.position));
return id?.kind === SyntaxKind.Identifier ? getLocation(id)?.range : undefined;
}
async function rename(params) {
if (isTspConfigFile(params.textDocument))
return { changes: {} };
const changes = {};
const result = await compileService.compile(params.textDocument);
if (result && result.document) {
const identifiers = findReferenceIdentifiers(result.program, result.script, result.document.offsetAt(params.position));
for (const id of identifiers) {
const location = getLocation(id);
if (!location) {
continue;
}
const change = TextEdit.replace(location.range, params.newName);
if (location.uri in changes) {
changes[location.uri].push(change);
}
else {
changes[location.uri] = [change];
}
}
}
return { changes };
}
function findReferenceIdentifiers(program, file, pos, searchFiles = program.sourceFiles.values()) {
const id = getNodeAtPosition(file, pos);
if (id?.kind !== SyntaxKind.Identifier) {
return [];
}
const sym = program.checker.resolveRelatedSymbols(id);
if (!sym || sym.length <= 0) {
return [id];
}
const references = [];
for (const searchFile of searchFiles) {
visitChildren(searchFile, function visit(node) {
if (node.kind === SyntaxKind.Identifier) {
const s = program.checker.resolveRelatedSymbols(node);
if (!s || s.length <= 0) {
return;
}
if (s[0] === sym[0] || (sym[0].type && s[0].type === sym[0].type)) {
references.push(node);
}
}
visitChildren(node, visit);
});
}
return references;
}
async function getSemanticTokensForDocument(params) {
if (isTspConfigFile(params.textDocument))
return [];
const ast = await compileService.getScript(params.textDocument);
if (!ast) {
return [];
}
return getSemanticTokens(ast);
}
async function buildSemanticTokens(params) {
if (isTspConfigFile(params.textDocument))
return { data: [] };
const builder = new SemanticTokensBuilder();
const tokens = await getSemanticTokensForDocument(params);
const file = await compilerHost.readFile(await fileService.getPath(params.textDocument));
const starts = file.getLineStarts();
for (const token of tokens) {
const start = file.getLineAndCharacterOfPosition(token.pos);
const end = file.getLineAndCharacterOfPosition(token.end);
for (let pos = token.pos, line = start.line; line <= end.line; line++) {
const endPos = line === end.line ? token.end : starts[line + 1];
const character = line === start.line ? start.character : 0;
builder.push(line, character, endPos - pos, token.kind, 0);
pos = endPos;
}
}
return builder.build();
}
async function getCodeActions(params) {
if (isTspConfigFile(params.textDocument))
return [];
const actions = [];
for (const vsDiag of params.context.diagnostics) {
const tspDiag = currentDiagnosticIndex.get(vsDiag.data?.id);
if (tspDiag === undefined || tspDiag.codefixes === undefined)
continue;
for (const fix of tspDiag.codefixes ?? []) {
const codeAction = {
...CodeAction.create(fix.label, {
title: fix.label,
command: Commands.APPLY_CODE_FIX,
arguments: [params.textDocument.uri, vsDiag.data?.id, fix.id],
}, CodeActionKind.QuickFix),
diagnostics: [vsDiag],
};
actions.push(codeAction);
}
}
return actions;
}
async function executeCommand(params) {
if (params.command === Commands.APPLY_CODE_FIX) {
const [documentUri, diagId, fixId] = params.arguments ?? [];
if (documentUri && diagId && fixId) {
const diag = currentDiagnosticIndex.get(diagId);
const codeFix = diag?.codefixes?.find((x) => x.id === fixId);
if (codeFix) {
const edits = await resolveCodeFix(codeFix);
const vsEdits = convertCodeFixEdits(edits);
await host.applyEdit({ changes: { [documentUri]: vsEdits } });
}
}
}
}
function convertCodeFixEdits(edits) {
return edits.map(convertCodeFixEdit);
}
function convertCodeFixEdit(edit) {
switch (edit.kind) {
case "insert-text":
return TextEdit.insert(edit.file.getLineAndCharacterOfPosition(edit.pos), edit.text);
case "replace-text":
return TextEdit.replace(getRange(edit, edit.file), edit.text);
}
}
function documentClosed(change) {
// clear diagnostics on file close
sendDiagnostics(change.document, []);
}
function getLocations(targets) {
return targets?.map(getLocation).filter((x) => !!x) ?? [];
}
function getLocation(target) {
const location = getSourceLocation(target);
if (location.isSynthetic) {
return undefined;
}
return {
uri: fileService.getURL(location.file.path),
range: getRange(location, location.file),
};
}
function getRange(location, file) {
const start = file.getLineAndCharacterOfPosition(location.pos);
const end = file.getLineAndCharacterOfPosition(location.end);
return Range.create(start, end);
}
function log(log) {
if (!isInitialized) {
pendingMessages.push(log);
return;
}
for (const pending of pendingMessages) {
host.log(pending);
}
pendingMessages = [];
host.log(log);
}
function sendDiagnostics(document, diagnostics) {
host.sendDiagnostics({
uri: document.uri,
version: document.version,
diagnostics,
});
}
function createCompilerHost() {
const base = host.compilerHost;
return {
...base,
parseCache: new WeakMap(),
readFile,
stat,
getSourceFileKind,
logSink: {
log: (log) => {
const msg = formatLog(log, { excludeLogLevel: true });
const sLog = {
level: log.level,
message: msg,
};
host.log(sLog);
},
},
};
async function readFile(path) {
const document = fileService.getOpenDocument(path);
const cached = await fileSystemCache.get(path);
// Try cache
if (cached && (!document || document.version === cached.version)) {
if (cached.type === "error") {
throw cached.error;
}
return cached.file;
}
// Try open document, although this is cheap, the instance still needs
// to be cached so that the compiler can reuse parse and bind results.
if (document) {
const file = {
document,
...createSourceFile(document.getText(), path),
};
fileSystemCache.set(path, { type: "file", file, version: document.version });
return file;
}
// Hit the disk and cache
try {
const file = await base.readFile(path);
fileSystemCache.set(path, { type: "file", file });
return file;
}
catch (error) {
fileSystemCache.set(path, { type: "error", error });
throw error;
}
}
async function stat(path) {
// if we have an open document for the path or a cache entry, then we know
// it's a file and not a directory and needn't hit the disk.
if (fileService.getOpenDocument(path) || (await fileSystemCache.get(path))?.type === "file") {
return {
isFile() {
return true;
},
isDirectory() {
return false;
},
};
}
return await base.stat(path);
}
function getSourceFileKind(path) {
const document = fileService.getOpenDocument(path);
if (document?.languageId === "typespec") {
return "typespec";
}
return getSourceFileKindFromExt(path);
}
}
}
function getSignatureHelpNodeAtPosition(script, position) {
// Move back over any trailing trivia. Otherwise, if there is no
// closing paren/angle bracket, we can find ourselves outside the desired
// node altogether in cases like `@dec(test, |`.
position = getPositionBeforeTrivia(script, position);
const node = getNodeAtPosition(script, position, (n) => {
switch (n.kind) {
case SyntaxKind.DecoratorExpression:
case SyntaxKind.AugmentDecoratorStatement:
case SyntaxKind.TypeReference:
// Do not consider node if positioned before the argument list.
// This is the standard behavior for signature help and further
// deals with nesting such as `Outer<Inner|> where we do not want
// we want help with the `Outer` arguments, not the `Inner` ones.
if (position <= n.target.end) {
return false;
}
// Likewise, no signature help at the end of argument list unless the
// it has no closing paren/angle bracket.
if (position === n.end) {
const endChar = script.file.text.charCodeAt(position - 1);
const closeChar = n.kind === SyntaxKind.TypeReference ? 62 /* CharCode.GreaterThan */ : 41 /* CharCode.CloseParen */;
if (endChar === closeChar) {
return false;
}
}
return true;
default:
return false;
}
});
if (!node) {
return undefined;
}
const argumentIndex = getSignatureHelpArgumentIndex(script, node, position);
if (argumentIndex < 0) {
return undefined;
}
return { node, argumentIndex };
}
function getSignatureHelpArgumentIndex(script, node, position) {
// Normalize arguments into a single list to avoid special case for
// augment decorators.
const args = node.kind === SyntaxKind.AugmentDecoratorStatement
? [node.targetType, ...node.arguments]
: node.arguments;
// Find the first argument that ends after the position. We don't look at
// the argument start position since the cursor might be in leading
// trivia, and we skip trivia to get the effective argument end position
// since the cursor might be in trailing trivia.
for (let i = 0; i < args.length; i++) {
if (position <= skipTrivia(script.file.text, args[i].end)) {
return i;
}
}
// If we reach here, we must be at the next argument after any that are in
// the syntax tree. There won't be a missing identifier for this argument
// in the tree since the parser error recovery will have assumed a
// trailing delimiter or empty list.
return args.length;
}
/**