Merge pull request #10 from github/cschleiden/expression-complete-if

Support expression completion for `if`
This commit is contained in:
Christopher Schleiden
2022-11-17 09:16:35 -08:00
committed by GitHub
3 changed files with 97 additions and 32 deletions
@@ -98,5 +98,31 @@ describe("expressions", () => {
expect(result.map(x => x.label)).toEqual(["arch", "name", "os", "temp", "tool_cache"]);
});
it("job if", async () => {
const input = `on: push
jobs:
build:
if: github.|
runs-on: ubuntu-latest
steps:
- run: echo`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["event"]);
});
it("step if", async () => {
const input = `on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo
if: github.|`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["event"]);
});
});
});
+18 -14
View File
@@ -11,16 +11,16 @@ import {CompletionItem} from "vscode-languageserver-types";
import {ContextProviderConfig} from "./context-providers/config";
import {getContext} from "./context-providers/default";
import {nullTrace} from "./nulltrace";
import {findInnerTokenAndParent} from "./utils/find-token";
import {findToken} from "./utils/find-token";
import {transform} from "./utils/transform";
import {Value, ValueProviderConfig} from "./value-providers/config";
import {defaultValueProviders} from "./value-providers/default";
export function getExpressionInput(input: string, pos: number): string | undefined {
export function getExpressionInput(input: string, pos: number): string {
// Find start marker around the cursor position
const startPos = input.lastIndexOf(OPEN_EXPRESSION, pos);
if (startPos === -1) {
return undefined;
return input;
}
// Find end marker after the cursor position
@@ -48,26 +48,30 @@ export async function complete(
};
const result = parseWorkflow(file.name, [file], nullTrace);
const [innerToken, parent] = findInnerTokenAndParent(newPos, result.value);
const {token, keyToken, parent} = findToken(newPos, result.value);
// 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.
if (innerToken && isString(innerToken) && innerToken.value.indexOf(OPEN_EXPRESSION) >= 0) {
// TODO: Handle expressions without markers like `if`
if (token) {
// We don't have any way of specifying that a token in the workflow schema is alwyas an expression. For now these
// are only the job and step level `if` nodes, so check for those here.
const isIfKey = keyToken && isString(keyToken) && keyToken.value === "if";
const containsExpression = isString(token) && token.value.indexOf(OPEN_EXPRESSION) >= 0;
if (isString(token) && (isIfKey || containsExpression)) {
const currentInput = token.value;
const currentInput = innerToken.value;
// Transform the overall position into a node relative position
const relCharPos = newPos.character - token.range!.start[1];
// Transform the overall position into a node relative position
const relCharPos = newPos.character - innerToken.range!.start[1];
const expressionInput = (getExpressionInput(currentInput, relCharPos) || "").trim();
const expressionInput = (getExpressionInput(currentInput, relCharPos) || "").trim();
const context = await getContext(token.definition?.readerContext || [], contextProviderConfig);
const context = await getContext(innerToken.definition?.readerContext || [], contextProviderConfig);
return completeExpression(expressionInput, context, []);
return completeExpression(expressionInput, context, []);
}
}
const values = await getValues(innerToken, parent, newPos, textDocument.uri, valueProviderConfig);
const values = await getValues(token, parent, newPos, textDocument.uri, valueProviderConfig);
return values.map(value => CompletionItem.create(value.label));
}
+53 -18
View File
@@ -5,24 +5,37 @@ import {TokenType} from "@github/actions-workflow-parser/templates/tokens/types"
import {Position} from "vscode-languageserver-textdocument";
export function findInnerToken(pos: Position, root?: TemplateToken) {
const [innerToken, _] = findInnerTokenAndParent(pos, root);
return innerToken;
const {token} = findToken(pos, root);
return token;
}
export function findInnerTokenAndParent(
pos: Position,
root?: TemplateToken
): [TemplateToken | null, TemplateToken | null] {
export type TokenResult = {
token: TemplateToken | null;
keyToken: TemplateToken | null;
parent: TemplateToken | null;
};
export function findToken(pos: Position, root?: TemplateToken): TokenResult {
if (!root) {
return [null, null];
return {
token: null,
keyToken: null,
parent: null
};
}
const s = [root];
let lastMatchingToken: TemplateToken | null = null;
let parent: TemplateToken | null = null;
const s: TokenResult[] = [
{
token: root,
keyToken: null,
parent: null
}
];
for (;;) {
const token = s.shift();
while (s.length > 0) {
const {parent, token, keyToken} = s.shift()!;
if (!token) {
break;
}
@@ -31,36 +44,58 @@ export function findInnerTokenAndParent(
continue;
}
// Pos is in token, remember this token
lastMatchingToken = token;
// Position is in token, enqueue children if there are any
switch (token.templateTokenType) {
case TokenType.Mapping:
const mappingToken = token as MappingToken;
parent = mappingToken;
for (let i = 0; i < mappingToken.count; i++) {
const {key, value} = mappingToken.get(i);
// Null tokens don't have a position, we can only use the line information
if (nullNodeOnLine(pos, key, value)) {
return [value, key];
return {
token: value,
keyToken: null,
parent: key
};
}
s.push(value);
s.push({
parent: mappingToken,
keyToken: key,
token: value
});
}
continue;
case TokenType.Sequence:
const sequenceToken = token as SequenceToken;
parent = sequenceToken;
for (let i = 0; i < sequenceToken.count; i++) {
s.push(sequenceToken.get(i));
s.push({
token: sequenceToken.get(i),
keyToken: null,
parent: sequenceToken
});
}
continue;
}
return [token, parent];
return {
token,
keyToken,
parent
};
}
return [null, parent];
// Did not find a matching token, return the last matching token as parent
return {
token: null,
parent: lastMatchingToken,
keyToken: null
};
}
function posInToken(pos: Position, token: TemplateToken): boolean {