diff --git a/actions-languageserver/src/description-provider.ts b/actions-languageserver/src/description-provider.ts index 9173a8e..128a1ee 100644 --- a/actions-languageserver/src/description-provider.ts +++ b/actions-languageserver/src/description-provider.ts @@ -1,11 +1,10 @@ import {DescriptionProvider} from "@github/actions-languageservice/hover"; import {Octokit} from "@octokit/rest"; import {getActionInputDescription} from "./description-providers/action-input"; -import {getReusableWorkflowInputDescription} from "./description-providers/reusable-workflow-input"; import {TTLCache} from "./utils/cache"; 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) { return undefined; } @@ -14,10 +13,6 @@ export function descriptionProvider(client: Octokit | undefined, cache: TTLCache if (context.step && parent.definition?.key === "step-with") { 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 { diff --git a/actions-languageserver/src/description-providers/reusable-workflow-input.ts b/actions-languageserver/src/description-providers/reusable-workflow-input.ts deleted file mode 100644 index e725b6e..0000000 --- a/actions-languageserver/src/description-providers/reusable-workflow-input.ts +++ /dev/null @@ -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 { - 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 "" -} diff --git a/actions-languageservice/src/hover.reusable-workflow.test.ts b/actions-languageservice/src/hover.reusable-workflow.test.ts index 3ddc69b..481a8b0 100644 --- a/actions-languageservice/src/hover.reusable-workflow.test.ts +++ b/actions-languageservice/src/hover.reusable-workflow.test.ts @@ -1,29 +1,8 @@ -import {isString} from "@github/actions-workflow-parser"; -import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token"; -import {CompletionItem, MarkupContent} from "vscode-languageserver-types"; -import {DescriptionProvider, hover, HoverConfig} from "./hover"; +import {hover} from "./hover"; +import {testHoverConfig} from "./hover.test"; import {getPositionFromCursor} from "./test-utils/cursor-position"; -import {testFileProvider} from "./test-utils/test-file-provider"; -export function testHoverReusableWorkflowConfig(tokenValue: string, tokenKey: string) { - 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", () => { +describe("hover on reusable workflows", () => { it("hover on job input with description", async () => { const input = ` on: push @@ -36,7 +15,7 @@ jobs: `; const result = await hover( ...getPositionFromCursor(input), - testHoverReusableWorkflowConfig("username", "scalar-needs-context") + testHoverConfig("username", "scalar-needs-context") ); expect(result).not.toBeUndefined(); expect(result?.contents).toEqual( diff --git a/actions-languageservice/src/hover.test.ts b/actions-languageservice/src/hover.test.ts index 7ec5afd..a7c4064 100644 --- a/actions-languageservice/src/hover.test.ts +++ b/actions-languageservice/src/hover.test.ts @@ -2,6 +2,7 @@ import {isString} from "@github/actions-workflow-parser"; import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token"; import {DescriptionProvider, hover, HoverConfig} from "./hover"; import {getPositionFromCursor} from "./test-utils/cursor-position"; +import {testFileProvider} from "./test-utils/test-file-provider"; export function testHoverConfig(tokenValue: string, tokenKey: string, description?: string) { return { @@ -16,7 +17,8 @@ export function testHoverConfig(tokenValue: string, tokenKey: string, descriptio return description; } - } satisfies DescriptionProvider + } satisfies DescriptionProvider, + fileProvider: testFileProvider } satisfies HoverConfig; } diff --git a/actions-languageservice/src/hover.ts b/actions-languageservice/src/hover.ts index e16a91b..dadbf03 100644 --- a/actions-languageservice/src/hover.ts +++ b/actions-languageservice/src/hover.ts @@ -2,10 +2,12 @@ import {DescriptionDictionary, Parser} from "@github/actions-expressions"; import {FunctionInfo} from "@github/actions-expressions/funcs/info"; import {Lexer} from "@github/actions-expressions/lexer"; 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 {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 {DESCRIPTION} from "@github/actions-workflow-parser/templates/template-constants"; import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token"; import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token"; import {isBasicExpression, isString} from "@github/actions-workflow-parser/templates/tokens/type-guards"; @@ -35,8 +37,7 @@ export type DescriptionProvider = { getDescription( context: WorkflowContext, token: TemplateToken, - path: TemplateToken[], - template?: WorkflowTemplate + path: TemplateToken[] ): Promise }; @@ -54,17 +55,17 @@ export async function hover(document: TextDocument, position: Position, config?: const {token, keyToken, parent} = tokenResult; 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 (isBasicExpression(token) || isPotentiallyExpression(token)) { info(`Calculating expression hover for token with definition ${tokenDefinitionInfo.definition.key}`); const allowedContext = tokenDefinitionInfo.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 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}`); - if (tokenResult.parent && isCronMappingValue(tokenResult)) { + if (tokenResult.parent && isCronMappingValue(tokenResult) && isString(token)) { const tokenValue = (token as StringToken).value; const description = getCronDescription(tokenValue); 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); - - 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(", ")}`; + if (tokenResult.parent && isReusableWorkflowJobInput(tokenResult)) { + let description = getReusableWorkflowInputDescription(workflowContext, tokenResult, template) + description = appendContext(token, description); + return { + contents: description, + range: mapRange(token.range) + } as Hover; } + let description = await getDescription(config, workflowContext, token, tokenResult.path); + + description = appendContext(token, description); + return { contents: description, range: mapRange(token.range) } 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( - document: TextDocument, config: HoverConfig | undefined, - result: ParseWorkflowResult | undefined, + workflowContext: WorkflowContext, token: TemplateToken, path: TemplateToken[] ) { const defaultDescription = token.description || ""; - // TODO fix this check - description provider is null for rusable workflows - if (!result?.value || !config?.descriptionProvider) { + if (!config?.descriptionProvider) { return defaultDescription; } - const template = await convertWorkflowTemplate(result.context, result.value, config?.fileProvider, { - 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); + const description = await config.descriptionProvider.getDescription(workflowContext, token, path); 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( exprPos: ExpressionPos, context: DescriptionDictionary, @@ -178,3 +193,36 @@ function expressionHover( 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 "" +} +