2022-11-14 22:37:34 +00:00
|
|
|
import { parseWorkflow } from "@github/actions-workflow-parser";
|
2022-11-08 17:00:59 -08:00
|
|
|
import {
|
|
|
|
|
SEQUENCE_TYPE,
|
|
|
|
|
STRING_TYPE,
|
|
|
|
|
MAPPING_TYPE,
|
|
|
|
|
TemplateToken,
|
|
|
|
|
NULL_TYPE,
|
|
|
|
|
} from "@github/actions-workflow-parser/templates/tokens/index";
|
|
|
|
|
import { MappingToken } from "@github/actions-workflow-parser/templates/tokens/mapping-token";
|
|
|
|
|
import { SequenceToken } from "@github/actions-workflow-parser/templates/tokens/sequence-token";
|
|
|
|
|
import { StringToken } from "@github/actions-workflow-parser/templates/tokens/string-token";
|
|
|
|
|
import { File } from "@github/actions-workflow-parser/workflows/file";
|
|
|
|
|
import { Position, TextDocument } from "vscode-languageserver-textdocument";
|
|
|
|
|
import { CompletionItem } from "vscode-languageserver-types";
|
|
|
|
|
import { nullTrace } from "./nulltrace";
|
|
|
|
|
import { findInnerTokenAndParent } from "./utils/find-token";
|
|
|
|
|
import { transform } from "./utils/transform";
|
|
|
|
|
import { Value, ValueProviderConfig } from "./value-providers/config";
|
|
|
|
|
import { defaultValueProviders } from "./value-providers/default";
|
|
|
|
|
|
|
|
|
|
export async function complete(
|
|
|
|
|
textDocument: TextDocument,
|
|
|
|
|
position: Position,
|
|
|
|
|
valueProviderConfig?: ValueProviderConfig
|
|
|
|
|
): Promise<CompletionItem[]> {
|
|
|
|
|
// Fix the input to work around YAML parsing issues
|
|
|
|
|
const [newDoc, newPos] = transform(textDocument, position);
|
|
|
|
|
|
|
|
|
|
const file: File = {
|
|
|
|
|
name: textDocument.uri,
|
|
|
|
|
content: newDoc.getText(),
|
|
|
|
|
};
|
|
|
|
|
const result = parseWorkflow(file.name, [file], nullTrace);
|
2022-11-14 22:34:57 +00:00
|
|
|
|
2022-11-14 22:37:34 +00:00
|
|
|
const [innerToken, parent] = findInnerTokenAndParent(newPos, result.value);
|
2022-11-08 17:00:59 -08:00
|
|
|
const values = await getValues(
|
|
|
|
|
innerToken,
|
|
|
|
|
parent,
|
|
|
|
|
newPos,
|
|
|
|
|
textDocument.uri,
|
2022-11-14 22:37:34 +00:00
|
|
|
valueProviderConfig
|
2022-11-08 17:00:59 -08:00
|
|
|
);
|
2022-11-14 22:37:34 +00:00
|
|
|
return values.map((value) => CompletionItem.create(value.label));
|
2022-11-08 17:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function getValues(
|
|
|
|
|
token: TemplateToken | null,
|
|
|
|
|
parent: TemplateToken | null,
|
|
|
|
|
position: Position,
|
|
|
|
|
workflowUri: string,
|
2022-11-14 22:37:34 +00:00
|
|
|
valueProviderConfig: ValueProviderConfig | undefined
|
2022-11-08 17:00:59 -08:00
|
|
|
): Promise<Value[]> {
|
|
|
|
|
if (!parent) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (token?.templateTokenType === NULL_TYPE) {
|
|
|
|
|
// Ensure there's a space after the parent key
|
|
|
|
|
if (parent.range && position.character + 1 === parent.range.end[1]) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const existingValues = getExistingValues(token, parent);
|
|
|
|
|
|
|
|
|
|
let customValues: Value[] | undefined = undefined;
|
|
|
|
|
if (token?.definition?.key) {
|
|
|
|
|
customValues = await valueProviderConfig?.getCustomValues(
|
|
|
|
|
token.definition.key,
|
2022-11-14 22:37:34 +00:00
|
|
|
{ uri: workflowUri }
|
2022-11-08 17:00:59 -08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (customValues !== undefined) {
|
|
|
|
|
return filterAndSortCompletionOptions(customValues, existingValues);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const valueProviders = defaultValueProviders();
|
|
|
|
|
|
|
|
|
|
// Use the key from the parent if we don't have a value provider for the current key
|
|
|
|
|
// Ideally each token would have a valid key
|
|
|
|
|
const valueProvider =
|
|
|
|
|
(token?.definition?.key && valueProviders[token.definition.key]) ||
|
|
|
|
|
(parent?.definition?.key && valueProviders[parent.definition.key]);
|
|
|
|
|
if (!valueProvider) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
const values = valueProvider();
|
|
|
|
|
|
|
|
|
|
return filterAndSortCompletionOptions(values, existingValues);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getExistingValues(token: TemplateToken | null, parent: TemplateToken) {
|
|
|
|
|
// For incomplete YAML, we may only have a parent token
|
|
|
|
|
if (token) {
|
|
|
|
|
if (
|
|
|
|
|
token.templateTokenType !== STRING_TYPE ||
|
|
|
|
|
parent.templateTokenType !== SEQUENCE_TYPE
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sequenceValues = new Set<string>();
|
|
|
|
|
const seqToken = parent as SequenceToken;
|
|
|
|
|
for (let i = 0; i < seqToken.count; i++) {
|
|
|
|
|
const t = seqToken.get(i);
|
|
|
|
|
if (t.isLiteral && t.templateTokenType === STRING_TYPE) {
|
|
|
|
|
// Should we support other literal values here?
|
|
|
|
|
sequenceValues.add((t as StringToken).value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return sequenceValues;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (parent.templateTokenType === MAPPING_TYPE) {
|
|
|
|
|
// No token and parent is a mapping, so we're completing a key
|
|
|
|
|
const mapKeys = new Set<string>();
|
|
|
|
|
const mapToken = parent as MappingToken;
|
|
|
|
|
for (let i = 0; i < mapToken.count; i++) {
|
|
|
|
|
const key = mapToken.get(i).key;
|
|
|
|
|
if (key.isLiteral && key.templateTokenType === STRING_TYPE) {
|
|
|
|
|
mapKeys.add((key as StringToken).value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return mapKeys;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filterAndSortCompletionOptions(
|
|
|
|
|
options: Value[],
|
|
|
|
|
existingValues?: Set<string>
|
|
|
|
|
) {
|
|
|
|
|
options = options.filter(
|
|
|
|
|
(x) => !existingValues || !existingValues.has(x.label)
|
|
|
|
|
);
|
|
|
|
|
options.sort((a, b) => a.label.localeCompare(b.label));
|
|
|
|
|
return options;
|
|
|
|
|
}
|