Files
languageservices/actions-languageservice/src/expression-hover/visitor.ts
T
2023-02-02 08:57:02 -08:00

167 lines
4.1 KiB
TypeScript

import {
DescriptionDictionary,
Evaluator,
isDescriptionDictionary,
wellKnownFunctions
} from "@github/actions-expressions";
import {
Binary,
ContextAccess,
Expr,
ExprVisitor,
FunctionCall,
Grouping,
IndexAccess,
Literal,
Logical,
Unary
} from "@github/actions-expressions/ast";
import {FunctionDefinition, FunctionInfo} from "@github/actions-expressions/funcs/info";
import {Pos, Range} from "@github/actions-expressions/lexer";
import {posWithinRange} from "./pos-range";
export type HoverResult =
| undefined
| {
label: string;
description?: string;
function: boolean;
range: Range;
};
export class HoverVisitor implements ExprVisitor<HoverResult> {
private ignorePosCheck = false;
constructor(
private pos: Pos,
private context: DescriptionDictionary,
private extensionFunctions: FunctionInfo[],
private functions: Map<string, FunctionDefinition>
) {}
hover(n: Expr): HoverResult {
return n.accept(this);
}
visitLiteral(literal: Literal): HoverResult {
return undefined;
}
visitUnary(unary: Unary): HoverResult {
return this.hover(unary.expr);
}
visitBinary(binary: Binary): HoverResult {
return this.hover(binary.left) || this.hover(binary.right);
}
visitLogical(logical: Logical): HoverResult {
for (const arg of logical.args) {
const result = this.hover(arg);
if (result) {
return result;
}
}
return undefined;
}
visitGrouping(grouping: Grouping): HoverResult {
return this.hover(grouping.group);
}
visitContextAccess(contextAccess: ContextAccess): HoverResult {
if (this.ignorePosCheck || posWithinRange(this.pos, contextAccess.name.range)) {
const contextName = contextAccess.name.lexeme;
return {
label: contextName,
description: this.context.getDescription(contextName),
function: false,
range: contextAccess.name.range
};
}
return undefined;
}
visitIndexAccess(indexAccess: IndexAccess): HoverResult {
// Is the position within the index, so for example:
// github.event.test
// ^ - pos
if (!(indexAccess.index instanceof Literal)) {
// No support for context access of the form github[github.event]
return undefined;
}
if (!posWithinRange(this.pos, indexAccess.index.token.range)) {
// Try to get hover from the rest of the expression
return this.hover(indexAccess.expr);
}
const ev = new Evaluator(indexAccess.expr, this.context, this.functions);
const result = ev.evaluate();
if (!isDescriptionDictionary(result)) {
// No description to show
return undefined;
}
const key = indexAccess.index.literal.coerceString();
const description = result.getDescription(key);
if (!description) {
return undefined;
}
// Calculate context access range for whole expression. For example:
// github.event.test
// ^ - pos
// should return the range:
// github.event.test
// ^^^^^^^^^^^^
this.ignorePosCheck = true;
try {
const contextHover = this.hover(indexAccess.expr);
if (!contextHover) {
throw new Error("Expected context hover to be defined");
}
return {
label: key,
description: description,
function: false,
range: {
start: contextHover.range.start,
end: indexAccess.index.token.range.end
}
};
} finally {
this.ignorePosCheck = false;
}
}
visitFunctionCall(functionCall: FunctionCall): HoverResult {
if (posWithinRange(this.pos, functionCall.functionName.range)) {
const functionName = functionCall.functionName.lexeme.toLowerCase();
const f = this.functions.get(functionName) || wellKnownFunctions[functionName];
return {
label: f.name,
description: f.description,
function: true,
range: functionCall.functionName.range
};
}
for (const args of functionCall.args) {
const result = this.hover(args);
if (result) {
return result;
}
}
return undefined;
}
}