Add needs value provider (#2)

* Add needs value provider
This commit is contained in:
Laura Yu
2022-11-23 10:54:15 -08:00
committed by GitHub
parent d4dd43dd0f
commit 330f62ebe7
6 changed files with 96 additions and 31 deletions
@@ -12,6 +12,21 @@ describe("completion", () => {
expect(result[0].label).toEqual("macos-10.13"); 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 () => { it("empty workflow", async () => {
const input = "|"; const input = "|";
const result = await complete(...getPositionFromCursor(input)); const result = await complete(...getPositionFromCursor(input));
+31 -15
View File
@@ -1,5 +1,11 @@
import {complete as completeExpression} from "@github/actions-expressions"; 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 {CLOSE_EXPRESSION, OPEN_EXPRESSION} from "@github/actions-workflow-parser/templates/template-constants";
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/index"; import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/index";
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token"; 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 {nullTrace} from "./nulltrace";
import {findToken} from "./utils/find-token"; import {findToken} from "./utils/find-token";
import {transform} from "./utils/transform"; 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 {defaultValueProviders} from "./value-providers/default";
import {definitionValues} from "./value-providers/definition"; import {definitionValues} from "./value-providers/definition";
@@ -48,8 +54,12 @@ export async function complete(
content: newDoc.getText() content: newDoc.getText()
}; };
const result = parseWorkflow(file.name, [file], nullTrace); 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 // 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.
@@ -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)); return values.map(value => CompletionItem.create(value.label));
} }
async function getValues( async function getValues(
token: TemplateToken | null, token: TemplateToken | null,
parent: TemplateToken | null, parent: TemplateToken | null,
workflowUri: string, parentKey: TemplateToken | null,
valueProviderConfig: ValueProviderConfig | undefined valueProviderConfig: ValueProviderConfig | undefined,
workflowContext: WorkflowContext
): Promise<Value[]> { ): Promise<Value[]> {
if (!parent) { if (!parent) {
return []; return [];
} }
const existingValues = getExistingValues(token, parent); const existingValues = getExistingValues(token, parent, parentKey);
let customValues: Value[] | undefined = undefined; let customValues: Value[] | undefined = undefined;
if (token?.definition?.key) { if (token?.definition?.key) {
customValues = await valueProviderConfig?.getCustomValues(token.definition.key, {uri: workflowUri}); customValues = await valueProviderConfig?.getCustomValues(token.definition.key, workflowContext);
} }
if (customValues !== undefined) { if (customValues !== undefined) {
return filterAndSortCompletionOptions(customValues, existingValues); 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 // Use the value provider from the parent if we don't have a value provider for the current key
const valueProvider = const valueProvider =
@@ -119,24 +131,28 @@ async function getValues(
return filterAndSortCompletionOptions(values, existingValues); 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 // For incomplete YAML, we may only have a parent token
if (token) { if (token) {
if (!isString(token) || !isSequence(parent)) { if (!isString(token)) {
return; return;
} }
if (isMapping(parent) && parentKey && isString(parentKey)) {
return new Set<string>([parentKey.value]);
}
if (isSequence(parent)) {
const sequenceValues = new Set<string>(); const sequenceValues = new Set<string>();
const seqToken = parent as SequenceToken; for (let i = 0; i < parent.count; i++) {
for (let i = 0; i < seqToken.count; i++) { const t = parent.get(i);
const t = seqToken.get(i); if (isString(t)) {
if (t.isLiteral && isString(t)) {
// Should we support other literal values here? // Should we support other literal values here?
sequenceValues.add(t.value); sequenceValues.add(t.value);
} }
} }
return sequenceValues; return sequenceValues;
} }
}
if (parent.templateTokenType === TokenType.Mapping) { if (parent.templateTokenType === TokenType.Mapping) {
// No token and parent is a mapping, so we're completing a key // No token and parent is a mapping, so we're completing a key
+20 -10
View File
@@ -14,6 +14,7 @@ export type TokenResult = {
token: TemplateToken | null; token: TemplateToken | null;
keyToken: TemplateToken | null; keyToken: TemplateToken | null;
parent: TemplateToken | null; parent: TemplateToken | null;
parentKey: TemplateToken | null;
}; };
export function findToken(pos: Position, root?: TemplateToken): TokenResult { export function findToken(pos: Position, root?: TemplateToken): TokenResult {
@@ -21,7 +22,8 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult {
return { return {
token: null, token: null,
keyToken: null, keyToken: null,
parent: null parent: null,
parentKey: null
}; };
} }
@@ -31,12 +33,13 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult {
{ {
token: root, token: root,
keyToken: null, keyToken: null,
parent: null parent: null,
parentKey: null
} }
]; ];
while (s.length > 0) { while (s.length > 0) {
const {parent, token, keyToken} = s.shift()!; const {parent, token, keyToken, parentKey} = s.shift()!;
if (!token) { if (!token) {
break; break;
} }
@@ -62,14 +65,16 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult {
return { return {
token: null, token: null,
keyToken: null, keyToken: null,
parent: null parent: null,
parentKey: null
}; };
} }
return { return {
token: key, token: key,
keyToken: null, keyToken: null,
parent: mappingToken parent: mappingToken,
parentKey: keyToken
}; };
} }
@@ -78,7 +83,8 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult {
return { return {
token: value, token: value,
keyToken: null, keyToken: null,
parent: key parent: key,
parentKey: keyToken
}; };
} }
} }
@@ -86,7 +92,8 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult {
s.push({ s.push({
token: value, token: value,
keyToken: key, keyToken: key,
parent: mappingToken parent: mappingToken,
parentKey: keyToken
}); });
} }
continue; continue;
@@ -97,7 +104,8 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult {
s.push({ s.push({
token: sequenceToken.get(i), token: sequenceToken.get(i),
keyToken: null, keyToken: null,
parent: sequenceToken parent: sequenceToken,
parentKey: null
}); });
} }
continue; continue;
@@ -106,7 +114,8 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult {
return { return {
token, token,
keyToken, keyToken,
parent parent,
parentKey
}; };
} }
@@ -114,7 +123,8 @@ export function findToken(pos: Position, root?: TemplateToken): TokenResult {
return { return {
token: null, token: null,
parent: lastMatchingToken, parent: lastMatchingToken,
keyToken: null keyToken: null,
parentKey: null
}; };
} }
@@ -1,3 +1,5 @@
import {WorkflowTemplate} from "@github/actions-workflow-parser/.";
export interface Value { export interface Value {
label: string; label: string;
description?: string; description?: string;
@@ -7,6 +9,7 @@ export type ValueProvider = () => Value[];
export interface WorkflowContext { export interface WorkflowContext {
uri: string; uri: string;
template: WorkflowTemplate | undefined;
} }
export interface ValueProviderConfig { export interface ValueProviderConfig {
getCustomValues: (key: string, context: WorkflowContext) => Promise<Value[] | undefined>; getCustomValues: (key: string, context: WorkflowContext) => Promise<Value[] | undefined>;
@@ -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 { return {
needs: () => getJobNames(workflowContext.template),
"runs-on": () => "runs-on": () =>
stringsToValues([ stringsToValues([
"ubuntu-latest", "ubuntu-latest",
@@ -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<string>();
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}));
}