Integrate expression hover
This commit is contained in:
@@ -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}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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, __) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user