diff --git a/actions-languageservice/src/complete.test.ts b/actions-languageservice/src/complete.test.ts index bf10a08..206f8c1 100644 --- a/actions-languageservice/src/complete.test.ts +++ b/actions-languageservice/src/complete.test.ts @@ -12,6 +12,21 @@ describe("completion", () => { expect(result[0].label).toEqual("macos-10.13"); }); + it("needs", async () => { + const input = `on: push +jobs: + build: + runs-on: ubuntu-latest + build2: + runs-on: ubuntu-latest + needs: bu|`; + const result = await complete(...getPositionFromCursor(input)); + + expect(result).not.toBeUndefined(); + expect(result.length).toEqual(1); + expect(result[0].label).toEqual("build"); + }); + it("empty workflow", async () => { const input = "|"; const result = await complete(...getPositionFromCursor(input)); diff --git a/actions-languageservice/src/complete.ts b/actions-languageservice/src/complete.ts index 15f1439..caaf767 100644 --- a/actions-languageservice/src/complete.ts +++ b/actions-languageservice/src/complete.ts @@ -1,5 +1,11 @@ import {complete as completeExpression} from "@github/actions-expressions"; -import {isSequence, isString, parseWorkflow} from "@github/actions-workflow-parser"; +import { + convertWorkflowTemplate, + isMapping, + isSequence, + isString, + parseWorkflow, +} from "@github/actions-workflow-parser"; import {CLOSE_EXPRESSION, OPEN_EXPRESSION} from "@github/actions-workflow-parser/templates/template-constants"; import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/index"; import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token"; @@ -13,7 +19,7 @@ import {getContext} from "./context-providers/default"; import {nullTrace} from "./nulltrace"; import {findToken} from "./utils/find-token"; import {transform} from "./utils/transform"; -import {Value, ValueProviderConfig} from "./value-providers/config"; +import {Value, ValueProviderConfig, WorkflowContext} from "./value-providers/config"; import {defaultValueProviders} from "./value-providers/default"; import {definitionValues} from "./value-providers/definition"; @@ -48,8 +54,12 @@ export async function complete( content: newDoc.getText() }; const result = parseWorkflow(file.name, [file], nullTrace); + if (!result.value) { + return []; + } - const {token, keyToken, parent} = findToken(newPos, result.value); + const {token, keyToken, parent, parentKey} = findToken(newPos, result.value); + const template = convertWorkflowTemplate(result.context, 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. @@ -72,32 +82,34 @@ export async function complete( } } - const values = await getValues(token, parent, textDocument.uri, valueProviderConfig); + const workflowContext = {uri: textDocument.uri, template: template}; + const values = await getValues(token, parent, parentKey, valueProviderConfig, workflowContext); return values.map(value => CompletionItem.create(value.label)); } async function getValues( token: TemplateToken | null, parent: TemplateToken | null, - workflowUri: string, - valueProviderConfig: ValueProviderConfig | undefined + parentKey: TemplateToken | null, + valueProviderConfig: ValueProviderConfig | undefined, + workflowContext: WorkflowContext ): Promise { if (!parent) { return []; } - const existingValues = getExistingValues(token, parent); + const existingValues = getExistingValues(token, parent, parentKey); let customValues: Value[] | undefined = undefined; if (token?.definition?.key) { - customValues = await valueProviderConfig?.getCustomValues(token.definition.key, {uri: workflowUri}); + customValues = await valueProviderConfig?.getCustomValues(token.definition.key, workflowContext); } if (customValues !== undefined) { return filterAndSortCompletionOptions(customValues, existingValues); } - const valueProviders = defaultValueProviders(); + const valueProviders = defaultValueProviders(workflowContext); // Use the value provider from the parent if we don't have a value provider for the current key const valueProvider = @@ -119,23 +131,27 @@ async function getValues( return filterAndSortCompletionOptions(values, existingValues); } -function getExistingValues(token: TemplateToken | null, parent: TemplateToken) { +function getExistingValues(token: TemplateToken | null, parent: TemplateToken, parentKey: TemplateToken | null) { // For incomplete YAML, we may only have a parent token if (token) { - if (!isString(token) || !isSequence(parent)) { + if (!isString(token)) { return; } - const sequenceValues = new Set(); - const seqToken = parent as SequenceToken; - for (let i = 0; i < seqToken.count; i++) { - const t = seqToken.get(i); - if (t.isLiteral && isString(t)) { - // Should we support other literal values here? - sequenceValues.add(t.value); + if (isMapping(parent) && parentKey && isString(parentKey)) { + return new Set([parentKey.value]); + } + if (isSequence(parent)) { + const sequenceValues = new Set(); + for (let i = 0; i < parent.count; i++) { + const t = parent.get(i); + if (isString(t)) { + // Should we support other literal values here? + sequenceValues.add(t.value); + } } + return sequenceValues; } - return sequenceValues; } if (parent.templateTokenType === TokenType.Mapping) { diff --git a/actions-languageservice/src/utils/find-token.ts b/actions-languageservice/src/utils/find-token.ts index 8d2cbfd..752a8c2 100644 --- a/actions-languageservice/src/utils/find-token.ts +++ b/actions-languageservice/src/utils/find-token.ts @@ -14,6 +14,7 @@ export type TokenResult = { token: TemplateToken | null; keyToken: TemplateToken | null; parent: TemplateToken | null; + parentKey: TemplateToken | null; }; export function findToken(pos: Position, root?: TemplateToken): TokenResult { @@ -21,7 +22,8 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult { return { token: null, keyToken: null, - parent: null + parent: null, + parentKey: null }; } @@ -31,12 +33,13 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult { { token: root, keyToken: null, - parent: null + parent: null, + parentKey: null } ]; while (s.length > 0) { - const {parent, token, keyToken} = s.shift()!; + const {parent, token, keyToken, parentKey} = s.shift()!; if (!token) { break; } @@ -62,14 +65,16 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult { return { token: null, keyToken: null, - parent: null + parent: null, + parentKey: null }; } return { token: key, keyToken: null, - parent: mappingToken + parent: mappingToken, + parentKey: keyToken }; } @@ -78,7 +83,8 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult { return { token: value, keyToken: null, - parent: key + parent: key, + parentKey: keyToken }; } } @@ -86,7 +92,8 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult { s.push({ token: value, keyToken: key, - parent: mappingToken + parent: mappingToken, + parentKey: keyToken }); } continue; @@ -97,7 +104,8 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult { s.push({ token: sequenceToken.get(i), keyToken: null, - parent: sequenceToken + parent: sequenceToken, + parentKey: null }); } continue; @@ -106,7 +114,8 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult { return { token, keyToken, - parent + parent, + parentKey }; } @@ -114,7 +123,8 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult { return { token: null, parent: lastMatchingToken, - keyToken: null + keyToken: null, + parentKey: null }; } diff --git a/actions-languageservice/src/value-providers/config.ts b/actions-languageservice/src/value-providers/config.ts index 48e9b70..45ab277 100644 --- a/actions-languageservice/src/value-providers/config.ts +++ b/actions-languageservice/src/value-providers/config.ts @@ -1,3 +1,5 @@ +import {WorkflowTemplate} from "@github/actions-workflow-parser/."; + export interface Value { label: string; description?: string; @@ -7,6 +9,7 @@ export type ValueProvider = () => Value[]; export interface WorkflowContext { uri: string; + template: WorkflowTemplate | undefined; } export interface ValueProviderConfig { getCustomValues: (key: string, context: WorkflowContext) => Promise; diff --git a/actions-languageservice/src/value-providers/default.ts b/actions-languageservice/src/value-providers/default.ts index 7fecea5..7fb1fe5 100644 --- a/actions-languageservice/src/value-providers/default.ts +++ b/actions-languageservice/src/value-providers/default.ts @@ -1,7 +1,9 @@ -import {Value, ValueProvider} from "./config"; +import {Value, ValueProvider, WorkflowContext} from "./config"; +import {getJobNames} from "./needs"; -export function defaultValueProviders(): {[key: string]: ValueProvider} { +export function defaultValueProviders(workflowContext: WorkflowContext): {[key: string]: ValueProvider} { return { + needs: () => getJobNames(workflowContext.template), "runs-on": () => stringsToValues([ "ubuntu-latest", diff --git a/actions-languageservice/src/value-providers/needs.ts b/actions-languageservice/src/value-providers/needs.ts new file mode 100644 index 0000000..eed60f5 --- /dev/null +++ b/actions-languageservice/src/value-providers/needs.ts @@ -0,0 +1,19 @@ +import {Value} from "./config"; +import {WorkflowTemplate} from "@github/actions-workflow-parser/model/workflow-template"; + +export function getJobNames(template: WorkflowTemplate | undefined): Value[] { + if (!template) { + return []; + } + + const jobNames = new Set(); + const jobList = template.jobs; + for (const job of jobList) { + const name = job.id; + if (name && !jobNames.has(name)) { + jobNames.add(name); + } + } + + return Array.from(jobNames).map(label => ({label})); +}