Move description fetching to language service
This commit is contained in:
@@ -1,11 +1,10 @@
|
|||||||
import {DescriptionProvider} from "@github/actions-languageservice/hover";
|
import {DescriptionProvider} from "@github/actions-languageservice/hover";
|
||||||
import {Octokit} from "@octokit/rest";
|
import {Octokit} from "@octokit/rest";
|
||||||
import {getActionInputDescription} from "./description-providers/action-input";
|
import {getActionInputDescription} from "./description-providers/action-input";
|
||||||
import {getReusableWorkflowInputDescription} from "./description-providers/reusable-workflow-input";
|
|
||||||
import {TTLCache} from "./utils/cache";
|
import {TTLCache} from "./utils/cache";
|
||||||
|
|
||||||
export function descriptionProvider(client: Octokit | undefined, cache: TTLCache): DescriptionProvider {
|
export function descriptionProvider(client: Octokit | undefined, cache: TTLCache): DescriptionProvider {
|
||||||
const getDescription: DescriptionProvider["getDescription"] = async (context, token, path, template) => {
|
const getDescription: DescriptionProvider["getDescription"] = async (context, token, path) => {
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -14,10 +13,6 @@ export function descriptionProvider(client: Octokit | undefined, cache: TTLCache
|
|||||||
if (context.step && parent.definition?.key === "step-with") {
|
if (context.step && parent.definition?.key === "step-with") {
|
||||||
return await getActionInputDescription(client, cache, context.step, token);
|
return await getActionInputDescription(client, cache, context.step, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.reusableWorkflowJob && template && parent.definition?.key === "workflow-job-with") {
|
|
||||||
return await getReusableWorkflowInputDescription(context.reusableWorkflowJob, token, template);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import {WorkflowTemplate, isMapping} from "@github/actions-workflow-parser";
|
|
||||||
import {isReusableWorkflowJob} from "@github/actions-workflow-parser/model/type-guards";
|
|
||||||
import {isString} from "@github/actions-workflow-parser";
|
|
||||||
import {ReusableWorkflowJob} from "@github/actions-workflow-parser/model/workflow-template";
|
|
||||||
import {DESCRIPTION} from "@github/actions-workflow-parser/templates/template-constants";
|
|
||||||
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
|
|
||||||
|
|
||||||
export async function getReusableWorkflowInputDescription(
|
|
||||||
reusableWorkflowJob: ReusableWorkflowJob,
|
|
||||||
token: TemplateToken,
|
|
||||||
template: WorkflowTemplate
|
|
||||||
): Promise<string | undefined> {
|
|
||||||
if (!isReusableWorkflowJob(reusableWorkflowJob)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputName = isString(token) && token.value;
|
|
||||||
if (!inputName) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out just reusable jobs
|
|
||||||
const templateReusableJobs = template.jobs.filter(isReusableWorkflowJob);
|
|
||||||
|
|
||||||
// Find the reusable job in the template that matches the current reusable job
|
|
||||||
const templateReusableJob = templateReusableJobs.find(job => job.id.value === reusableWorkflowJob.id.value);
|
|
||||||
|
|
||||||
// Find the input description in the template, if any
|
|
||||||
if (templateReusableJob && reusableWorkflowJob["input-definitions"] && templateReusableJob["input-definitions"]) {
|
|
||||||
const definition = templateReusableJob["input-definitions"].find(token.value)
|
|
||||||
if (definition && isMapping(definition)) {
|
|
||||||
const description = definition.find(DESCRIPTION)
|
|
||||||
if (description && isString(description)) {
|
|
||||||
return description.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
@@ -1,29 +1,8 @@
|
|||||||
import {isString} from "@github/actions-workflow-parser";
|
import {hover} from "./hover";
|
||||||
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
|
import {testHoverConfig} from "./hover.test";
|
||||||
import {CompletionItem, MarkupContent} from "vscode-languageserver-types";
|
|
||||||
import {DescriptionProvider, hover, HoverConfig} from "./hover";
|
|
||||||
import {getPositionFromCursor} from "./test-utils/cursor-position";
|
import {getPositionFromCursor} from "./test-utils/cursor-position";
|
||||||
import {testFileProvider} from "./test-utils/test-file-provider";
|
|
||||||
|
|
||||||
export function testHoverReusableWorkflowConfig(tokenValue: string, tokenKey: string) {
|
describe("hover on reusable workflows", () => {
|
||||||
return {
|
|
||||||
descriptionProvider: {
|
|
||||||
getDescription: async (_, token, __) => {
|
|
||||||
if (!isString(token)) {
|
|
||||||
throw new Error("Test provider only supports string tokens");
|
|
||||||
}
|
|
||||||
|
|
||||||
expect((token as StringToken).value).toEqual(tokenValue);
|
|
||||||
expect(token.definition!.key).toEqual(tokenKey);
|
|
||||||
|
|
||||||
return token.description;
|
|
||||||
}
|
|
||||||
} satisfies DescriptionProvider,
|
|
||||||
fileProvider: testFileProvider
|
|
||||||
} satisfies HoverConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("completion with reusable workflows", () => {
|
|
||||||
it("hover on job input with description", async () => {
|
it("hover on job input with description", async () => {
|
||||||
const input = `
|
const input = `
|
||||||
on: push
|
on: push
|
||||||
@@ -36,7 +15,7 @@ jobs:
|
|||||||
`;
|
`;
|
||||||
const result = await hover(
|
const result = await hover(
|
||||||
...getPositionFromCursor(input),
|
...getPositionFromCursor(input),
|
||||||
testHoverReusableWorkflowConfig("username", "scalar-needs-context")
|
testHoverConfig("username", "scalar-needs-context")
|
||||||
);
|
);
|
||||||
expect(result).not.toBeUndefined();
|
expect(result).not.toBeUndefined();
|
||||||
expect(result?.contents).toEqual(
|
expect(result?.contents).toEqual(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {isString} from "@github/actions-workflow-parser";
|
|||||||
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
|
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
|
||||||
import {DescriptionProvider, hover, HoverConfig} from "./hover";
|
import {DescriptionProvider, hover, HoverConfig} from "./hover";
|
||||||
import {getPositionFromCursor} from "./test-utils/cursor-position";
|
import {getPositionFromCursor} from "./test-utils/cursor-position";
|
||||||
|
import {testFileProvider} from "./test-utils/test-file-provider";
|
||||||
|
|
||||||
export function testHoverConfig(tokenValue: string, tokenKey: string, description?: string) {
|
export function testHoverConfig(tokenValue: string, tokenKey: string, description?: string) {
|
||||||
return {
|
return {
|
||||||
@@ -16,7 +17,8 @@ export function testHoverConfig(tokenValue: string, tokenKey: string, descriptio
|
|||||||
|
|
||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
} satisfies DescriptionProvider
|
} satisfies DescriptionProvider,
|
||||||
|
fileProvider: testFileProvider
|
||||||
} satisfies HoverConfig;
|
} satisfies HoverConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import {DescriptionDictionary, Parser} from "@github/actions-expressions";
|
|||||||
import {FunctionInfo} from "@github/actions-expressions/funcs/info";
|
import {FunctionInfo} from "@github/actions-expressions/funcs/info";
|
||||||
import {Lexer} from "@github/actions-expressions/lexer";
|
import {Lexer} from "@github/actions-expressions/lexer";
|
||||||
import {convertWorkflowTemplate, parseWorkflow, ParseWorkflowResult} from "@github/actions-workflow-parser";
|
import {convertWorkflowTemplate, parseWorkflow, ParseWorkflowResult} from "@github/actions-workflow-parser";
|
||||||
import {WorkflowTemplate} from "@github/actions-workflow-parser";
|
import {WorkflowTemplate, isMapping} from "@github/actions-workflow-parser";
|
||||||
import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert";
|
import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert";
|
||||||
import {getCronDescription} from "@github/actions-workflow-parser/model/converter/cron";
|
import {getCronDescription} from "@github/actions-workflow-parser/model/converter/cron";
|
||||||
|
import {isReusableWorkflowJob} from "@github/actions-workflow-parser/model/type-guards";
|
||||||
import {splitAllowedContext} from "@github/actions-workflow-parser/templates/allowed-context";
|
import {splitAllowedContext} from "@github/actions-workflow-parser/templates/allowed-context";
|
||||||
|
import {DESCRIPTION} from "@github/actions-workflow-parser/templates/template-constants";
|
||||||
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
|
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
|
||||||
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
|
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
|
||||||
import {isBasicExpression, isString} from "@github/actions-workflow-parser/templates/tokens/type-guards";
|
import {isBasicExpression, isString} from "@github/actions-workflow-parser/templates/tokens/type-guards";
|
||||||
@@ -35,8 +37,7 @@ export type DescriptionProvider = {
|
|||||||
getDescription(
|
getDescription(
|
||||||
context: WorkflowContext,
|
context: WorkflowContext,
|
||||||
token: TemplateToken,
|
token: TemplateToken,
|
||||||
path: TemplateToken[],
|
path: TemplateToken[]
|
||||||
template?: WorkflowTemplate
|
|
||||||
): Promise<string | undefined>
|
): Promise<string | undefined>
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,17 +55,17 @@ export async function hover(document: TextDocument, position: Position, config?:
|
|||||||
const {token, keyToken, parent} = tokenResult;
|
const {token, keyToken, parent} = tokenResult;
|
||||||
|
|
||||||
const tokenDefinitionInfo = (keyToken || parent || token)?.definitionInfo;
|
const tokenDefinitionInfo = (keyToken || parent || token)?.definitionInfo;
|
||||||
|
const template = await convertWorkflowTemplate(result.context, result.value, config?.fileProvider, {
|
||||||
|
errorPolicy: ErrorPolicy.TryConversion,
|
||||||
|
fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0,
|
||||||
|
});
|
||||||
|
const workflowContext = getWorkflowContext(document.uri, template, tokenResult.path);
|
||||||
if (token && tokenDefinitionInfo) {
|
if (token && tokenDefinitionInfo) {
|
||||||
if (isBasicExpression(token) || isPotentiallyExpression(token)) {
|
if (isBasicExpression(token) || isPotentiallyExpression(token)) {
|
||||||
info(`Calculating expression hover for token with definition ${tokenDefinitionInfo.definition.key}`);
|
info(`Calculating expression hover for token with definition ${tokenDefinitionInfo.definition.key}`);
|
||||||
|
|
||||||
const allowedContext = tokenDefinitionInfo.allowedContext || [];
|
const allowedContext = tokenDefinitionInfo.allowedContext || [];
|
||||||
const {namedContexts, functions} = splitAllowedContext(allowedContext);
|
const {namedContexts, functions} = splitAllowedContext(allowedContext);
|
||||||
|
|
||||||
const template = await convertWorkflowTemplate(result.context, result.value, undefined, {
|
|
||||||
errorPolicy: ErrorPolicy.TryConversion
|
|
||||||
});
|
|
||||||
const workflowContext = getWorkflowContext(document.uri, template, tokenResult.path);
|
|
||||||
const context = await getContext(namedContexts, config?.contextProviderConfig, workflowContext, Mode.Completion);
|
const context = await getContext(namedContexts, config?.contextProviderConfig, workflowContext, Mode.Completion);
|
||||||
|
|
||||||
const exprPos = mapToExpressionPos(token, position);
|
const exprPos = mapToExpressionPos(token, position);
|
||||||
@@ -80,7 +81,7 @@ export async function hover(document: TextDocument, position: Position, config?:
|
|||||||
|
|
||||||
info(`Calculating hover for token with definition ${token.definition.key}`);
|
info(`Calculating hover for token with definition ${token.definition.key}`);
|
||||||
|
|
||||||
if (tokenResult.parent && isCronMappingValue(tokenResult)) {
|
if (tokenResult.parent && isCronMappingValue(tokenResult) && isString(token)) {
|
||||||
const tokenValue = (token as StringToken).value;
|
const tokenValue = (token as StringToken).value;
|
||||||
const description = getCronDescription(tokenValue);
|
const description = getCronDescription(tokenValue);
|
||||||
if (description) {
|
if (description) {
|
||||||
@@ -91,39 +92,46 @@ export async function hover(document: TextDocument, position: Position, config?:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let description = await getDescription(document, config, result, token, tokenResult.path);
|
if (tokenResult.parent && isReusableWorkflowJobInput(tokenResult)) {
|
||||||
|
let description = getReusableWorkflowInputDescription(workflowContext, tokenResult, template)
|
||||||
const allowedContext = token.definitionInfo?.allowedContext;
|
description = appendContext(token, description);
|
||||||
if (allowedContext && allowedContext?.length > 0) {
|
return {
|
||||||
// Only add padding if there is a description
|
contents: description,
|
||||||
description += `${description.length > 0 ? `\n\n` : ""}**Context:** ${allowedContext.join(", ")}`;
|
range: mapRange(token.range)
|
||||||
|
} as Hover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let description = await getDescription(config, workflowContext, token, tokenResult.path);
|
||||||
|
|
||||||
|
description = appendContext(token, description);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contents: description,
|
contents: description,
|
||||||
range: mapRange(token.range)
|
range: mapRange(token.range)
|
||||||
} satisfies Hover;
|
} satisfies Hover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendContext(token: TemplateToken, description: string) {
|
||||||
|
const allowedContext = token.definitionInfo?.allowedContext;
|
||||||
|
if (allowedContext && allowedContext?.length > 0) {
|
||||||
|
// Only add padding if there is a description
|
||||||
|
description += `${description.length > 0 ? `\n\n` : ""}**Context:** ${allowedContext.join(", ")}`;
|
||||||
|
}
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
async function getDescription(
|
async function getDescription(
|
||||||
document: TextDocument,
|
|
||||||
config: HoverConfig | undefined,
|
config: HoverConfig | undefined,
|
||||||
result: ParseWorkflowResult | undefined,
|
workflowContext: WorkflowContext,
|
||||||
token: TemplateToken,
|
token: TemplateToken,
|
||||||
path: TemplateToken[]
|
path: TemplateToken[]
|
||||||
) {
|
) {
|
||||||
const defaultDescription = token.description || "";
|
const defaultDescription = token.description || "";
|
||||||
// TODO fix this check - description provider is null for rusable workflows
|
if (!config?.descriptionProvider) {
|
||||||
if (!result?.value || !config?.descriptionProvider) {
|
|
||||||
return defaultDescription;
|
return defaultDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
const template = await convertWorkflowTemplate(result.context, result.value, config?.fileProvider, {
|
const description = await config.descriptionProvider.getDescription(workflowContext, token, path);
|
||||||
errorPolicy: ErrorPolicy.TryConversion,
|
|
||||||
fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0,
|
|
||||||
});
|
|
||||||
const workflowContext = getWorkflowContext(document.uri, template, path);
|
|
||||||
const description = await config.descriptionProvider.getDescription(workflowContext, token, path, template);
|
|
||||||
return description || defaultDescription;
|
return description || defaultDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +143,13 @@ function isCronMappingValue(tokenResult: TokenResult): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isReusableWorkflowJobInput(tokenResult: TokenResult): boolean {
|
||||||
|
return (
|
||||||
|
tokenResult.parent?.definition?.key === "workflow-job-with" &&
|
||||||
|
isString(tokenResult.token!)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function expressionHover(
|
function expressionHover(
|
||||||
exprPos: ExpressionPos,
|
exprPos: ExpressionPos,
|
||||||
context: DescriptionDictionary,
|
context: DescriptionDictionary,
|
||||||
@@ -178,3 +193,36 @@ function expressionHover(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getReusableWorkflowInputDescription(workflowContext: WorkflowContext, tokenResult: TokenResult, template: WorkflowTemplate): string {
|
||||||
|
const reusableWorkflowJob = workflowContext.reusableWorkflowJob
|
||||||
|
|
||||||
|
if (reusableWorkflowJob && !isReusableWorkflowJob(reusableWorkflowJob)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputName = tokenResult.token && isString(tokenResult.token) && tokenResult.token.value;
|
||||||
|
if (!inputName) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out just reusable jobs
|
||||||
|
const templateJobs = template.jobs.filter(isReusableWorkflowJob);
|
||||||
|
|
||||||
|
// Find the reusable job in the template that matches the current reusable job
|
||||||
|
const templateJob = templateJobs.find(job => job.id.value === reusableWorkflowJob!.id.value);
|
||||||
|
|
||||||
|
// Find the input description in the template, if any
|
||||||
|
if (templateJob && reusableWorkflowJob!["input-definitions"] && templateJob["input-definitions"]) {
|
||||||
|
const definition = templateJob["input-definitions"].find((tokenResult.token! as StringToken).value)
|
||||||
|
if (definition && isMapping(definition)) {
|
||||||
|
const description = definition.find(DESCRIPTION)
|
||||||
|
if (description && isString(description)) {
|
||||||
|
return description.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user