Merge pull request #10 from github/cschleiden/expression-complete-if
Support expression completion for `if`
This commit is contained in:
@@ -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"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user