171 lines
4.9 KiB
TypeScript
171 lines
4.9 KiB
TypeScript
import {DescriptionPair} from "./completion/descriptionDictionary";
|
|
import {Dictionary, isDictionary} from "./data/dictionary";
|
|
import {ExpressionData} from "./data/expressiondata";
|
|
import {Evaluator} from "./evaluator";
|
|
import {wellKnownFunctions} from "./funcs";
|
|
import {FunctionDefinition, FunctionInfo} from "./funcs/info";
|
|
import {Lexer, Token, TokenType} from "./lexer";
|
|
import {Parser} from "./parser";
|
|
|
|
export type CompletionItem = {
|
|
label: string;
|
|
description?: string;
|
|
function: boolean;
|
|
};
|
|
|
|
/**
|
|
* Complete returns a list of completion items for the given expression.
|
|
* The main functionality is auto-completing functions and context access:
|
|
* We can only provide assistance if the input is in one of the following forms (with | denoting the cursor position):
|
|
* - context.path.inp| or context.path['inp| -- auto-complete context access
|
|
* - context.path.| or context.path['| -- auto-complete context access
|
|
* - toJS| -- auto-complete function call or top-level
|
|
* - | -- auto-complete function call or top-level context access
|
|
*
|
|
* @param input Input expression
|
|
* @param context Context available for the expression
|
|
* @param extensionFunctions List of functions available
|
|
* @param functions Optional map of functions to use during evaluation
|
|
* @returns Array of completion items
|
|
*/
|
|
export function complete(
|
|
input: string,
|
|
context: Dictionary,
|
|
extensionFunctions: FunctionInfo[],
|
|
functions?: Map<string, FunctionDefinition>
|
|
): CompletionItem[] {
|
|
// Lex
|
|
const lexer = new Lexer(input);
|
|
const lexResult = lexer.lex();
|
|
|
|
// Find interesting part of the tokenVector. For example, for an expression like `github.actor == env.actor.log|`, we are
|
|
// only interested in the `env.actor.log` part for auto-completion
|
|
const tokenInputVector = trimTokenVector(lexResult.tokens);
|
|
|
|
// Start by skipping the EOF token
|
|
let tokenIdx = tokenInputVector.length - 2;
|
|
|
|
if (tokenIdx >= 0) {
|
|
switch (tokenInputVector[tokenIdx].type) {
|
|
// If there is a (partial) identifier under the cursor, ignore that
|
|
case TokenType.IDENTIFIER:
|
|
tokenIdx--;
|
|
break;
|
|
|
|
case TokenType.STRING:
|
|
// TODO: Support string for `context.name['test|`
|
|
return [];
|
|
}
|
|
}
|
|
|
|
if (tokenIdx < 0) {
|
|
// Vector only contains the EOF token. Suggest functions and root context access
|
|
const result = contextKeys(context);
|
|
|
|
// Merge with functions
|
|
result.push(...functionItems(extensionFunctions));
|
|
|
|
return result;
|
|
}
|
|
|
|
// Determine path that led to the last token
|
|
// Use parser & evaluator to determine context to complete.
|
|
const pathTokenVector = tokenInputVector.slice(0, tokenIdx);
|
|
|
|
// Include the original EOF token to make the parser happy
|
|
pathTokenVector.push(tokenInputVector[tokenInputVector.length - 1]);
|
|
|
|
const p = new Parser(
|
|
pathTokenVector,
|
|
context.pairs().map(x => x.key),
|
|
extensionFunctions
|
|
);
|
|
const expr = p.parse();
|
|
|
|
const ev = new Evaluator(expr, context, functions);
|
|
const result = ev.evaluate();
|
|
|
|
return contextKeys(result);
|
|
}
|
|
|
|
function functionItems(extensionFunctions: FunctionInfo[]): CompletionItem[] {
|
|
const result: CompletionItem[] = [];
|
|
|
|
for (const fdef of [...Object.values(wellKnownFunctions), ...extensionFunctions]) {
|
|
result.push({
|
|
label: fdef.name,
|
|
description: fdef.description,
|
|
function: true
|
|
});
|
|
}
|
|
|
|
// Sort functions
|
|
result.sort((a, b) => a.label.localeCompare(b.label));
|
|
|
|
return result;
|
|
}
|
|
|
|
function contextKeys(context: ExpressionData): CompletionItem[] {
|
|
if (isDictionary(context)) {
|
|
return (
|
|
context
|
|
.pairs()
|
|
.map(x => completionItemFromContext(x))
|
|
// Sort contexts
|
|
.sort((a, b) => a.label.localeCompare(b.label))
|
|
);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
function completionItemFromContext(pair: DescriptionPair): CompletionItem {
|
|
const context = pair.key.toString();
|
|
const parenIndex = context.indexOf("(");
|
|
const isFunc = parenIndex >= 0 && context.indexOf(")") >= 0;
|
|
|
|
return {
|
|
label: isFunc ? context.substring(0, parenIndex) : context,
|
|
description: pair.description,
|
|
function: isFunc
|
|
};
|
|
}
|
|
|
|
export function trimTokenVector(tokenVector: Token[]): Token[] {
|
|
let tokenIdx = tokenVector.length;
|
|
|
|
let openParen = 0;
|
|
|
|
while (tokenIdx > 0) {
|
|
const token = tokenVector[tokenIdx - 1];
|
|
switch (token.type) {
|
|
case TokenType.LEFT_PAREN:
|
|
if (openParen == 0) {
|
|
// Encounterend an open parenthesis witout a closing first, stop here
|
|
break;
|
|
}
|
|
tokenIdx--;
|
|
continue;
|
|
|
|
case TokenType.RIGHT_PAREN:
|
|
openParen++;
|
|
tokenIdx--;
|
|
continue;
|
|
|
|
case TokenType.IDENTIFIER:
|
|
case TokenType.DOT:
|
|
case TokenType.EOF:
|
|
case TokenType.LEFT_BRACKET:
|
|
case TokenType.RIGHT_BRACKET:
|
|
case TokenType.STRING:
|
|
tokenIdx--;
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
// Only keep the part of the token vector we're interested in
|
|
return tokenVector.slice(tokenIdx);
|
|
}
|