Integrate expression hover

This commit is contained in:
Christopher Schleiden
2023-02-02 08:57:26 -08:00
parent f5463c18b5
commit cb71496867
3 changed files with 208 additions and 13 deletions
@@ -0,0 +1,116 @@
import {data, DescriptionDictionary} from "@github/actions-expressions";
import {Hover} from "vscode-languageserver-types";
import {ContextProviderConfig} from "./context-providers/config";
import {hover} from "./hover";
import {registerLogger} from "./log";
import {getPositionFromCursor} from "./test-utils/cursor-position";
import {TestLogger} from "./test-utils/logger";
const contextProviderConfig: ContextProviderConfig = {
getContext: async (context: string) => {
switch (context) {
case "github":
return new DescriptionDictionary(
{
key: "event",
value: new data.StringData("push"),
description: "The event that triggered the workflow"
},
{
key: "test",
value: new DescriptionDictionary({
key: "name",
value: new data.StringData("push"),
description: "Name for the test"
}),
description: "Test dictionary"
}
);
}
return undefined;
}
};
registerLogger(new TestLogger());
describe("hover.expressions", () => {
it("context access", async () => {
const input = `on: push
run-name: \${{ github.even|t }}
jobs:
build:
runs-on: [self-hosted]`;
const result = await hover(...getPositionFromCursor(input), {
contextProviderConfig
});
expect(result).toEqual<Hover>({
contents: "The event that triggered the workflow",
range: {
start: {line: 1, character: 14},
end: {line: 1, character: 26}
}
});
});
it("context", async () => {
const input = `on: push
run-name: \${{ git|hub.event }}
jobs:
build:
runs-on: [self-hosted]`;
const result = await hover(...getPositionFromCursor(input), {
contextProviderConfig
});
expect(result).toEqual<Hover>({
contents:
"Information about the workflow run. For more information, see [`github` context](https://docs.github.com/actions/learn-github-actions/contexts#github-context).",
range: {
start: {line: 1, character: 14},
end: {line: 1, character: 20}
}
});
});
it("multiple expressions", async () => {
const input = `on: push
run-name: \${{ git|hub.event }}-\${{ github.event }}
jobs:
build:
runs-on: [self-hosted]`;
const result = await hover(...getPositionFromCursor(input), {
contextProviderConfig
});
expect(result).toEqual<Hover>({
contents:
"Information about the workflow run. For more information, see [`github` context](https://docs.github.com/actions/learn-github-actions/contexts#github-context).",
range: {
start: {line: 1, character: 14},
end: {line: 1, character: 20}
}
});
});
it("multi-line expression", async () => {
const input = `on: push
jobs:
build:
runs-on: [self-hosted]
steps:
- run: |
echo 'hello'
echo '\${{ github.test.na|me }}
echo 'world'
echo '\${{ github.event.test }}`;
const result = await hover(...getPositionFromCursor(input, 1), {
contextProviderConfig
});
expect(result).toEqual<Hover>({
contents: "Name for the test",
range: {
start: {line: 7, character: 18},
end: {line: 7, character: 34}
}
});
});
});
+1 -1
View File
@@ -3,7 +3,7 @@ import {StringToken} from "@github/actions-workflow-parser/templates/tokens/stri
import {DescriptionProvider, hover, HoverConfig} from "./hover";
import {getPositionFromCursor} from "./test-utils/cursor-position";
function testHoverConfig(tokenValue: string, tokenKey: string, description?: string) {
export function testHoverConfig(tokenValue: string, tokenKey: string, description?: string) {
return {
descriptionProvider: {
getDescription: async (_, token, __) => {
+91 -12
View File
@@ -1,37 +1,72 @@
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 {ErrorPolicy} from "@github/actions-workflow-parser/model/convert";
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
import {TokenResult} from "./utils/find-token";
import {File} from "@github/actions-workflow-parser/workflows/file";
import {isString} from "@github/actions-workflow-parser/templates/tokens/type-guards";
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";
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";
import {Position, TextDocument} from "vscode-languageserver-textdocument";
import {Hover} from "vscode-languageserver-types";
import {ContextProviderConfig} from "./context-providers/config";
import {getContext, Mode} from "./context-providers/default";
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
import {ExpressionPos, mapToExpressionPos} from "./expression-hover/expression-pos";
import {HoverVisitor} from "./expression-hover/visitor";
import {validatorFunctions} from "./expression-validation/functions";
import {info} from "./log";
import {nullTrace} from "./nulltrace";
import {findToken} from "./utils/find-token";
import {findToken, TokenResult} from "./utils/find-token";
import {mapRange} from "./utils/range";
import {getCronDescription} from "@github/actions-workflow-parser/model/converter/cron";
import {isStringExpression} from "./utils/type-guards";
export type HoverConfig = {
descriptionProvider?: DescriptionProvider;
contextProviderConfig?: ContextProviderConfig;
};
export type DescriptionProvider = {
getDescription(context: WorkflowContext, token: TemplateToken, path: TemplateToken[]): Promise<string | undefined>;
};
export type HoverConfig = {
descriptionProvider?: DescriptionProvider;
};
// Render value description and Context when hovering over a key in a MappingToken
export async function hover(document: TextDocument, position: Position, config?: HoverConfig): Promise<Hover | null> {
const file: File = {
name: document.uri,
content: document.getText()
};
const result = parseWorkflow(file.name, [file], nullTrace);
if (!result.value) {
return null;
}
const tokenResult = findToken(position, result.value);
const token = tokenResult.token;
if (config?.contextProviderConfig && token && tokenResult.keyToken?.definition) {
const isStringExpressionToken = isStringExpression(token);
const isBasicExpressionToken = isBasicExpression(token);
if (isStringExpressionToken || isBasicExpressionToken) {
info(`Calculating expression hover for token with definition ${tokenResult.keyToken.definition.key}`);
const allowedContext =
token.definitionInfo?.allowedContext || tokenResult.keyToken?.definitionInfo?.allowedContext || [];
const {namedContexts, functions} = splitAllowedContext(allowedContext);
const template = convertWorkflowTemplate(result.context, result.value, 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);
if (exprPos) {
return expressionHover(exprPos, context, namedContexts, functions);
}
}
}
if (!token?.definition) {
return null;
}
@@ -40,7 +75,7 @@ export async function hover(document: TextDocument, position: Position, config?:
if (tokenResult.parent && isCronMappingValue(tokenResult)) {
const tokenValue = (token as StringToken).value;
let description = getCronDescription(tokenValue);
const description = getCronDescription(tokenValue);
if (description) {
return {
contents: description,
@@ -88,3 +123,47 @@ function isCronMappingValue(tokenResult: TokenResult): boolean {
tokenResult.token.value !== "cron"
);
}
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();
const v = new HoverVisitor(position, context, [], validatorFunctions);
const hoverResult = v.hover(expr);
if (!hoverResult) {
return null;
}
const mr = hoverResult.range;
return {
contents: hoverResult?.description || hoverResult?.label,
// Map the expression range back to a document range
range: {
start: {
line: documentRange.start.line + mr.start.line,
character: documentRange.start.character + mr.start.column
},
end: {
line: documentRange.start.line + mr.end.line,
character: documentRange.start.character + mr.end.column
}
}
};
} catch (e) {
// Hovering over an invalid expression should not cause an error here
info(`Encountered error trying to calculate expression hover: ${e}`);
return null;
}
}