Support completion for partial expressions (#103)
Adding basic expression auto complete
This commit is contained in:
@@ -255,6 +255,13 @@ jobs:
|
|||||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("auto-complete partial", async () => {
|
||||||
|
const input = "run-name: ${{ github.ev| }}";
|
||||||
|
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||||
|
|
||||||
|
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("using default context provider", async () => {
|
it("using default context provider", async () => {
|
||||||
const input =
|
const input =
|
||||||
"on: push\njobs:\n build:\n runs-on: ubuntu-latest\n environment:\n url: ${{ runner.| }}\n steps:\n - run: echo";
|
"on: push\njobs:\n build:\n runs-on: ubuntu-latest\n environment:\n url: ${{ runner.| }}\n steps:\n - run: echo";
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import {complete as completeExpression} from "@github/actions-expressions";
|
import {DescriptionDictionary, complete as completeExpression} from "@github/actions-expressions";
|
||||||
import {CompletionItem as ExpressionCompletionItem} from "@github/actions-expressions/completion";
|
import {CompletionItem as ExpressionCompletionItem} from "@github/actions-expressions/completion";
|
||||||
import {convertWorkflowTemplate, isSequence, isString, parseWorkflow} from "@github/actions-workflow-parser";
|
import {
|
||||||
|
convertWorkflowTemplate,
|
||||||
|
isBasicExpression,
|
||||||
|
isSequence,
|
||||||
|
isString,
|
||||||
|
parseWorkflow
|
||||||
|
} from "@github/actions-workflow-parser";
|
||||||
import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert";
|
import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert";
|
||||||
import {DefinitionType} from "@github/actions-workflow-parser/templates/schema/definition-type";
|
import {DefinitionType} from "@github/actions-workflow-parser/templates/schema/definition-type";
|
||||||
import {StringDefinition} from "@github/actions-workflow-parser/templates/schema/string-definition";
|
import {StringDefinition} from "@github/actions-workflow-parser/templates/schema/string-definition";
|
||||||
@@ -24,6 +30,7 @@ import {transform} from "./utils/transform";
|
|||||||
import {Value, ValueProviderConfig} from "./value-providers/config";
|
import {Value, ValueProviderConfig} from "./value-providers/config";
|
||||||
import {defaultValueProviders} from "./value-providers/default";
|
import {defaultValueProviders} from "./value-providers/default";
|
||||||
import {definitionValues} from "./value-providers/definition";
|
import {definitionValues} from "./value-providers/definition";
|
||||||
|
import {TokenRange} from "@github/actions-workflow-parser/templates/tokens/token-range";
|
||||||
|
|
||||||
export function getExpressionInput(input: string, pos: number): string {
|
export function getExpressionInput(input: string, pos: number): string {
|
||||||
// Find start marker around the cursor position
|
// Find start marker around the cursor position
|
||||||
@@ -71,37 +78,14 @@ export async function complete(
|
|||||||
// If we are inside an expression, take a different code-path. The workflow parser does not correctly create
|
// If we are inside an expression, take a different code-path. The workflow parser does not correctly create
|
||||||
// expression nodes for invalid expressions and during editing expressions are invalid most of the time.
|
// expression nodes for invalid expressions and during editing expressions are invalid most of the time.
|
||||||
if (token) {
|
if (token) {
|
||||||
const isExpression =
|
const isStringExpressionToken = isStringExpression(token);
|
||||||
token.definition?.definitionType === DefinitionType.String && (token.definition as StringDefinition).isExpression;
|
const isBasicExpressionToken = isBasicExpression(token) && token.isExpression;
|
||||||
const containsExpression = isString(token) && token.value.indexOf(OPEN_EXPRESSION) >= 0;
|
|
||||||
if (isString(token) && (isExpression || containsExpression)) {
|
|
||||||
const currentInput = token.source || token.value;
|
|
||||||
|
|
||||||
// Transform the overall position into a node relative position
|
|
||||||
let relCharPos: number = 0;
|
|
||||||
const range = mapRange(token.range!);
|
|
||||||
if (range.start.line !== range.end.line) {
|
|
||||||
const lines = currentInput.split("\n");
|
|
||||||
const lineDiff = newPos.line - range.start.line - 1;
|
|
||||||
const linesBeforeCusor = lines.slice(0, lineDiff);
|
|
||||||
relCharPos = linesBeforeCusor.join("\n").length + newPos.character + 1;
|
|
||||||
} else {
|
|
||||||
relCharPos = newPos.character - range.start.character;
|
|
||||||
}
|
|
||||||
|
|
||||||
const expressionInput = (getExpressionInput(currentInput, relCharPos) || "").trim();
|
|
||||||
|
|
||||||
|
if (isStringExpressionToken || isBasicExpressionToken) {
|
||||||
const allowedContext = token.definitionInfo?.allowedContext || [];
|
const allowedContext = token.definitionInfo?.allowedContext || [];
|
||||||
const context = await getContext(allowedContext, contextProviderConfig, workflowContext, Mode.Completion);
|
const context = await getContext(allowedContext, contextProviderConfig, workflowContext, Mode.Completion);
|
||||||
|
|
||||||
try {
|
return getExpressionCompletionItems(token, context, newPos);
|
||||||
return completeExpression(expressionInput, context, [], validatorFunctions).map(item =>
|
|
||||||
mapExpressionCompletionItem(item, currentInput[relCharPos])
|
|
||||||
);
|
|
||||||
} catch (e: any) {
|
|
||||||
error(`Error while completing expression: '${e?.message || "<no details>"}'`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +200,35 @@ export function getExistingValues(token: TemplateToken | null, parent: TemplateT
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getExpressionCompletionItems(
|
||||||
|
token: TemplateToken,
|
||||||
|
context: DescriptionDictionary,
|
||||||
|
pos: Position
|
||||||
|
): CompletionItem[] {
|
||||||
|
let expressionInput = "";
|
||||||
|
let currentInput = "";
|
||||||
|
let relCharPos: number = 0;
|
||||||
|
|
||||||
|
if (isBasicExpression(token)) {
|
||||||
|
expressionInput = currentInput = token.expression;
|
||||||
|
relCharPos = getRelCharPos(token.range!, expressionInput, pos);
|
||||||
|
} else {
|
||||||
|
const stringToken = token.assertString("Expected string token for expression completion");
|
||||||
|
currentInput = stringToken.source || stringToken.value;
|
||||||
|
relCharPos = getRelCharPos(stringToken.range!, currentInput, pos);
|
||||||
|
expressionInput = (getExpressionInput(currentInput, relCharPos) || "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return completeExpression(expressionInput, context, [], validatorFunctions).map(item =>
|
||||||
|
mapExpressionCompletionItem(item, currentInput[relCharPos])
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
error(`Error while completing expression: '${e?.message || "<no details>"}'`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function filterAndSortCompletionOptions(options: Value[], existingValues?: Set<string>) {
|
function filterAndSortCompletionOptions(options: Value[], existingValues?: Set<string>) {
|
||||||
options = options.filter(x => !existingValues?.has(x.label));
|
options = options.filter(x => !existingValues?.has(x.label));
|
||||||
options.sort((a, b) => a.label.localeCompare(b.label));
|
options.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
@@ -239,3 +252,23 @@ function mapExpressionCompletionItem(item: ExpressionCompletionItem, charAfterPo
|
|||||||
kind: item.function ? CompletionItemKind.Function : CompletionItemKind.Variable
|
kind: item.function ? CompletionItemKind.Function : CompletionItemKind.Variable
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRelCharPos(tokenRange: TokenRange, currentInput: string, pos: Position): number {
|
||||||
|
// Transform the overall position into a node relative position
|
||||||
|
const range = mapRange(tokenRange);
|
||||||
|
if (range.start.line !== range.end.line) {
|
||||||
|
const lines = currentInput.split("\n");
|
||||||
|
const lineDiff = pos.line - range.start.line - 1;
|
||||||
|
const linesBeforeCusor = lines.slice(0, lineDiff);
|
||||||
|
return linesBeforeCusor.join("\n").length + pos.character + 1;
|
||||||
|
} else {
|
||||||
|
return pos.character - range.start.character;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStringExpression(token: TemplateToken): boolean {
|
||||||
|
const isExpression =
|
||||||
|
token.definition?.definitionType === DefinitionType.String && (token.definition as StringDefinition).isExpression;
|
||||||
|
const containsExpression = isString(token) && token.value.indexOf(OPEN_EXPRESSION) >= 0;
|
||||||
|
return isExpression || containsExpression;
|
||||||
|
}
|
||||||
|
|||||||
@@ -455,7 +455,7 @@ class TemplateReader {
|
|||||||
// Doesn't contain "${{"
|
// Doesn't contain "${{"
|
||||||
// Check if value should still be evaluated as an expression
|
// Check if value should still be evaluated as an expression
|
||||||
if (definitionInfo.definition instanceof StringDefinition && definitionInfo.definition.isExpression) {
|
if (definitionInfo.definition instanceof StringDefinition && definitionInfo.definition.isExpression) {
|
||||||
const expression = this.parseIntoExpressionToken(token.range!, raw, allowedContext, token);
|
const expression = this.parseIntoExpressionToken(token.range!, raw, allowedContext, token, definitionInfo);
|
||||||
if (expression) {
|
if (expression) {
|
||||||
return expression;
|
return expression;
|
||||||
}
|
}
|
||||||
@@ -520,7 +520,7 @@ class TemplateReader {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const expression = this.parseIntoExpressionToken(tr, rawExpression, allowedContext, token);
|
const expression = this.parseIntoExpressionToken(tr, rawExpression, allowedContext, token, definitionInfo);
|
||||||
|
|
||||||
if (!expression) {
|
if (!expression) {
|
||||||
// Record that we've hit an error but continue to validate any other expressions
|
// Record that we've hit an error but continue to validate any other expressions
|
||||||
@@ -615,9 +615,10 @@ class TemplateReader {
|
|||||||
tr: TokenRange,
|
tr: TokenRange,
|
||||||
rawExpression: string,
|
rawExpression: string,
|
||||||
allowedContext: string[],
|
allowedContext: string[],
|
||||||
token: TemplateToken
|
token: TemplateToken,
|
||||||
|
definitionInfo: DefinitionInfo | undefined
|
||||||
): ExpressionToken | undefined {
|
): ExpressionToken | undefined {
|
||||||
const parseExpressionResult = this.parseExpression(tr, rawExpression, allowedContext, token.definitionInfo);
|
const parseExpressionResult = this.parseExpression(tr, rawExpression, allowedContext, definitionInfo);
|
||||||
|
|
||||||
// Check for error
|
// Check for error
|
||||||
if (parseExpressionResult.error) {
|
if (parseExpressionResult.error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user