@@ -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));
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user