Files
languageservices/languageservice/src/hover.ts
T

200 lines
7.2 KiB
TypeScript
Raw Normal View History

2023-02-02 08:57:26 -08:00
import {DescriptionDictionary, Parser} from "@github/actions-expressions";
import {FunctionInfo} from "@github/actions-expressions/funcs/info";
import {Lexer} from "@github/actions-expressions/lexer";
2023-01-23 16:02:13 -05:00
import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert";
2023-02-02 08:57:26 -08:00
import {getCronDescription} from "@github/actions-workflow-parser/model/converter/cron";
import {splitAllowedContext} from "@github/actions-workflow-parser/templates/allowed-context";
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
2023-02-02 08:57:26 -08:00
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
import {isBasicExpression, isString} from "@github/actions-workflow-parser/templates/tokens/type-guards";
import {File} from "@github/actions-workflow-parser/workflows/file";
2023-02-15 14:50:26 -08:00
import {FileProvider} from "@github/actions-workflow-parser/workflows/file-provider";
2022-11-15 14:08:12 -05:00
import {Position, TextDocument} from "vscode-languageserver-textdocument";
import {Hover} from "vscode-languageserver-types";
2023-02-02 08:57:26 -08:00
import {ContextProviderConfig} from "./context-providers/config";
import {getContext, Mode} from "./context-providers/default";
2023-01-23 16:02:13 -05:00
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
2023-02-21 15:24:51 -08:00
import {
isReusableWorkflowJobInput,
getReusableWorkflowInputDescription
} from "./description-providers/reusable-job-inputs";
2023-02-02 08:57:26 -08:00
import {ExpressionPos, mapToExpressionPos} from "./expression-hover/expression-pos";
import {HoverVisitor} from "./expression-hover/visitor";
import {validatorFunctions} from "./expression-validation/functions";
2022-12-05 10:05:41 -08:00
import {info} from "./log";
2023-02-02 13:14:02 -08:00
import {isPotentiallyExpression} from "./utils/expression-detection";
2023-02-02 08:57:26 -08:00
import {findToken, TokenResult} from "./utils/find-token";
2022-12-07 10:51:44 -05:00
import {mapRange} from "./utils/range";
2023-03-03 12:39:40 -08:00
import {fetchOrParseWorkflow, fetchOrConvertWorkflowTemplate} from "./utils/workflow-cache";
2023-02-02 08:57:26 -08:00
export type HoverConfig = {
descriptionProvider?: DescriptionProvider;
contextProviderConfig?: ContextProviderConfig;
2023-02-15 14:50:26 -08:00
fileProvider?: FileProvider;
2023-02-02 08:57:26 -08:00
};
2022-11-08 17:00:59 -08:00
2023-01-23 16:02:13 -05:00
export type DescriptionProvider = {
2023-02-17 13:04:33 -08:00
getDescription(context: WorkflowContext, token: TemplateToken, path: TemplateToken[]): Promise<string | undefined>;
2023-01-23 16:02:13 -05:00
};
export async function hover(document: TextDocument, position: Position, config?: HoverConfig): Promise<Hover | null> {
2022-11-08 17:00:59 -08:00
const file: File = {
name: document.uri,
2022-11-15 14:08:12 -05:00
content: document.getText()
2022-11-08 17:00:59 -08:00
};
2023-03-03 12:28:29 -08:00
2023-03-03 12:39:40 -08:00
const parsedWorkflow = fetchOrParseWorkflow(file, document.uri);
2023-03-03 12:28:29 -08:00
if (!parsedWorkflow) {
2023-02-02 08:57:26 -08:00
return null;
}
2022-11-08 17:00:59 -08:00
2023-03-06 11:33:28 -08:00
const template = await fetchOrConvertWorkflowTemplate(parsedWorkflow, document.uri, config, {
errorPolicy: ErrorPolicy.TryConversion,
2023-02-17 13:04:33 -08:00
fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0
});
2023-03-03 12:28:29 -08:00
const tokenResult = findToken(position, parsedWorkflow.value);
const {token, keyToken, parent} = tokenResult;
const tokenDefinitionInfo = (keyToken || parent || token)?.definitionInfo;
const workflowContext = getWorkflowContext(document.uri, template, tokenResult.path);
if (token && tokenDefinitionInfo) {
2023-02-02 13:14:02 -08:00
if (isBasicExpression(token) || isPotentiallyExpression(token)) {
info(`Calculating expression hover for token with definition ${tokenDefinitionInfo.definition.key}`);
2023-02-02 08:57:26 -08:00
2023-02-02 13:14:02 -08:00
const allowedContext = tokenDefinitionInfo.allowedContext || [];
2023-02-02 08:57:26 -08:00
const {namedContexts, functions} = splitAllowedContext(allowedContext);
const context = await getContext(namedContexts, config?.contextProviderConfig, workflowContext, Mode.Completion);
2023-02-02 08:57:26 -08:00
const exprPos = mapToExpressionPos(token, position);
if (exprPos) {
2023-02-21 15:58:45 -08:00
return expressionHover(exprPos, context, namedContexts, functions);
2023-02-02 08:57:26 -08:00
}
}
}
2023-01-23 16:02:13 -05:00
if (!token?.definition) {
return null;
}
2023-01-23 16:02:13 -05:00
info(`Calculating hover for token with definition ${token.definition.key}`);
2023-02-17 12:38:20 -08:00
if (tokenResult.parent && isCronMappingValue(tokenResult)) {
2023-01-30 10:57:40 -08:00
const tokenValue = (token as StringToken).value;
2023-02-02 08:57:26 -08:00
const description = getCronDescription(tokenValue);
if (description) {
return {
contents: description,
range: mapRange(token.range)
2023-02-21 10:13:18 -08:00
} satisfies Hover;
}
}
2022-12-05 10:05:41 -08:00
if (tokenResult.parent && isReusableWorkflowJobInput(tokenResult)) {
2023-02-21 09:10:51 -08:00
let description = getReusableWorkflowInputDescription(workflowContext, tokenResult);
2023-02-21 15:19:03 -08:00
description = appendContext(description, token.definitionInfo?.allowedContext);
return {
contents: description,
range: mapRange(token.range)
2023-02-21 10:13:18 -08:00
} satisfies Hover;
2022-11-08 17:00:59 -08:00
}
2023-01-23 16:02:13 -05:00
let description = await getDescription(config, workflowContext, token, tokenResult.path);
2023-02-21 15:19:03 -08:00
description = appendContext(description, token.definitionInfo?.allowedContext);
2023-01-23 16:02:13 -05:00
return {
contents: description,
range: mapRange(token.range)
} satisfies Hover;
2022-11-08 17:00:59 -08:00
}
2023-02-21 15:19:03 -08:00
function appendContext(description: string, allowedContext?: string[]) {
2023-03-09 17:10:23 -05:00
if (!allowedContext || allowedContext.length == 0) {
return description;
}
let {namedContexts, functions} = splitAllowedContext(allowedContext);
let namedContextsString = "";
let functionsString = "";
2023-03-09 17:10:23 -05:00
if (namedContexts.length > 0) {
namedContextsString = `${description.length > 0 ? `\n\n` : ""}Available expression contexts: ${namedContexts
.map(x => `\`${x}\``)
.join(", ")}`;
}
if (functions.length > 0) {
functionsString = `${namedContexts.length > 0 ? `\n\n` : ""}Available expression functions: ${functions
.map(x => x.name)
.map(x => `\`${x}\``)
.join(", ")}`;
2023-03-09 17:10:23 -05:00
}
return `${description}${namedContextsString}${functionsString}`;
}
2023-01-23 16:02:13 -05:00
async function getDescription(
config: HoverConfig | undefined,
workflowContext: WorkflowContext,
2023-01-23 16:02:13 -05:00
token: TemplateToken,
path: TemplateToken[]
) {
const defaultDescription = token.description || "";
if (!config?.descriptionProvider) {
2023-01-23 16:02:13 -05:00
return defaultDescription;
2022-11-08 17:00:59 -08:00
}
2023-01-23 16:02:13 -05:00
const description = await config.descriptionProvider.getDescription(workflowContext, token, path);
2023-01-23 16:02:13 -05:00
return description || defaultDescription;
2022-11-08 17:00:59 -08:00
}
2023-01-23 16:53:29 -08:00
function isCronMappingValue(tokenResult: TokenResult): boolean {
2023-01-30 10:57:40 -08:00
return (
tokenResult.parent?.definition?.key === "cron-mapping" &&
2023-01-24 11:14:35 -08:00
isString(tokenResult.token!) &&
tokenResult.token.value !== "cron"
2023-01-30 10:57:40 -08:00
);
2023-01-23 16:25:30 -08:00
}
2023-02-02 08:57:26 -08:00
function expressionHover(
exprPos: ExpressionPos,
context: DescriptionDictionary,
namedContexts: string[],
functions: FunctionInfo[]
): Hover | null {
const {expression, position, documentRange} = exprPos;
try {
const l = new Lexer(expression);
const lr = l.lex();
const p = new Parser(lr.tokens, namedContexts, functions);
const expr = p.parse();
2023-02-02 13:02:34 -08:00
const hv = new HoverVisitor(position, context, [], validatorFunctions);
const hoverResult = hv.hover(expr);
2023-02-02 08:57:26 -08:00
if (!hoverResult) {
return null;
}
2023-02-02 13:02:34 -08:00
const exprRange = hoverResult.range;
2023-02-02 08:57:26 -08:00
return {
contents: hoverResult?.description || hoverResult?.label,
// Map the expression range back to a document range
range: {
start: {
2023-02-02 13:02:34 -08:00
line: documentRange.start.line + exprRange.start.line,
character: documentRange.start.character + exprRange.start.column
2023-02-02 08:57:26 -08:00
},
end: {
2023-02-02 13:02:34 -08:00
line: documentRange.start.line + exprRange.end.line,
character: documentRange.start.character + exprRange.end.column
2023-02-02 08:57:26 -08:00
}
}
};
} catch (e) {
// Hovering over an invalid expression should not cause an error here
info(`Encountered error trying to calculate expression hover: ${e}`);
return null;
}
}