diff --git a/actions-languageservice/src/complete.expressions.test.ts b/actions-languageservice/src/complete.expressions.test.ts index 274fb45..fd6eee0 100644 --- a/actions-languageservice/src/complete.expressions.test.ts +++ b/actions-languageservice/src/complete.expressions.test.ts @@ -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"]); + }); }); }); diff --git a/actions-languageservice/src/complete.ts b/actions-languageservice/src/complete.ts index 9722fa1..7286edc 100644 --- a/actions-languageservice/src/complete.ts +++ b/actions-languageservice/src/complete.ts @@ -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)); } diff --git a/actions-languageservice/src/utils/find-token.ts b/actions-languageservice/src/utils/find-token.ts index 48caa60..cdb12b7 100644 --- a/actions-languageservice/src/utils/find-token.ts +++ b/actions-languageservice/src/utils/find-token.ts @@ -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 {