Files
languageservices/actions-languageservice/src/complete.ts
T

178 lines
6.5 KiB
TypeScript
Raw Normal View History

2022-11-15 15:59:14 -08:00
import {complete as completeExpression} from "@github/actions-expressions";
2022-11-23 15:35:34 -08:00
import {convertWorkflowTemplate, isSequence, isString, parseWorkflow} from "@github/actions-workflow-parser";
2022-11-23 17:16:34 -08:00
import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert";
2022-11-15 13:03:44 -08:00
import {CLOSE_EXPRESSION, OPEN_EXPRESSION} from "@github/actions-workflow-parser/templates/template-constants";
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/index";
2022-11-15 14:08:12 -05:00
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token";
2022-11-15 13:03:44 -08:00
import {TokenType} from "@github/actions-workflow-parser/templates/tokens/types";
2022-11-15 14:08:12 -05:00
import {File} from "@github/actions-workflow-parser/workflows/file";
import {Position, TextDocument} from "vscode-languageserver-textdocument";
import {CompletionItem} from "vscode-languageserver-types";
2022-11-15 13:03:44 -08:00
import {ContextProviderConfig} from "./context-providers/config";
2022-11-15 15:59:14 -08:00
import {getContext} from "./context-providers/default";
2022-11-23 15:35:34 -08:00
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
2022-11-15 14:08:12 -05:00
import {nullTrace} from "./nulltrace";
2022-11-16 16:19:48 -08:00
import {findToken} from "./utils/find-token";
2022-11-15 14:08:12 -05:00
import {transform} from "./utils/transform";
2022-11-23 15:35:34 -08:00
import {Value, ValueProviderConfig} from "./value-providers/config";
2022-11-15 14:08:12 -05:00
import {defaultValueProviders} from "./value-providers/default";
import {definitionValues} from "./value-providers/definition";
2022-11-08 17:00:59 -08:00
2022-11-16 16:19:48 -08:00
export function getExpressionInput(input: string, pos: number): string {
2022-11-15 13:03:44 -08:00
// Find start marker around the cursor position
const startPos = input.lastIndexOf(OPEN_EXPRESSION, pos);
if (startPos === -1) {
2022-11-16 16:19:48 -08:00
return input;
2022-11-15 13:03:44 -08:00
}
// Find end marker after the cursor position
let endPos = input.indexOf(CLOSE_EXPRESSION, pos);
if (endPos === -1) {
// Assume an unfinished expression like "${{ someinput.|"
endPos = input.length;
}
return input.substring(startPos + OPEN_EXPRESSION.length, endPos);
}
2022-11-08 17:00:59 -08:00
export async function complete(
textDocument: TextDocument,
position: Position,
2022-11-15 13:03:44 -08:00
valueProviderConfig?: ValueProviderConfig,
contextProviderConfig?: ContextProviderConfig
2022-11-08 17:00:59 -08:00
): Promise<CompletionItem[]> {
2022-11-23 07:08:50 -08:00
// Edge case: when completing a key like `foo:|`, do not calculate auto-completions
const charBeforePos = textDocument.getText({
start: {line: position.line, character: position.character - 1},
end: {line: position.line, character: position.character}
});
if (charBeforePos === ":") {
return [];
}
2022-11-08 17:00:59 -08:00
// Fix the input to work around YAML parsing issues
const [newDoc, newPos] = transform(textDocument, position);
const file: File = {
name: textDocument.uri,
2022-11-15 14:08:12 -05:00
content: newDoc.getText()
2022-11-08 17:00:59 -08:00
};
const result = parseWorkflow(file.name, [file], nullTrace);
2022-11-23 10:54:15 -08:00
if (!result.value) {
return [];
}
2022-11-14 22:34:57 +00:00
2022-11-23 15:35:34 -08:00
const {token, keyToken, parent, path} = findToken(newPos, result.value);
2022-11-23 17:16:34 -08:00
const template = convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
2022-11-15 13:03:44 -08:00
// 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.
2022-11-16 16:19:48 -08:00
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;
2022-11-15 13:03:44 -08:00
2022-11-16 16:19:48 -08:00
// Transform the overall position into a node relative position
const relCharPos = newPos.character - token.range!.start[1];
2022-11-15 13:03:44 -08:00
2022-11-16 16:19:48 -08:00
const expressionInput = (getExpressionInput(currentInput, relCharPos) || "").trim();
2022-11-15 13:03:44 -08:00
2022-11-23 14:35:31 -08:00
const context = getContext(token.definition?.readerContext || [], contextProviderConfig);
2022-11-15 13:03:44 -08:00
2022-11-16 16:19:48 -08:00
return completeExpression(expressionInput, context, []);
}
2022-11-15 13:03:44 -08:00
}
2022-11-23 15:35:34 -08:00
const workflowContext = getWorkflowContext(textDocument.uri, template, path);
const values = await getValues(token, parent, valueProviderConfig, workflowContext);
2022-11-15 14:08:12 -05: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,
2022-11-23 10:54:15 -08:00
valueProviderConfig: ValueProviderConfig | undefined,
workflowContext: WorkflowContext
2022-11-08 17:00:59 -08:00
): Promise<Value[]> {
if (!parent) {
return [];
}
2022-11-23 15:35:34 -08:00
const existingValues = getExistingValues(token, parent);
2022-11-08 17:00:59 -08:00
if (token?.definition?.key) {
2022-11-28 16:15:57 -08:00
const customValues = await valueProviderConfig?.[token.definition.key]?.get(workflowContext);
2022-11-08 17:00:59 -08:00
2022-11-23 14:34:44 -08:00
if (customValues) {
return filterAndSortCompletionOptions(customValues, existingValues);
}
2022-11-08 17:00:59 -08:00
}
// Use the value provider from the parent if we don't have a value provider for the current key
2022-11-08 17:00:59 -08:00
const valueProvider =
2022-11-23 14:34:44 -08:00
(token?.definition?.key && defaultValueProviders[token.definition.key]) ||
(parent.definition?.key && defaultValueProviders[parent.definition.key]);
if (valueProvider) {
2022-11-28 16:15:57 -08:00
const values = await valueProvider.get(workflowContext);
return filterAndSortCompletionOptions(values, existingValues);
}
// Use the definition if there are no value providers
const def = token?.definition || parent.definition;
if (!def) {
2022-11-08 17:00:59 -08:00
return [];
}
const values = definitionValues(def);
2022-11-08 17:00:59 -08:00
return filterAndSortCompletionOptions(values, existingValues);
}
2022-11-28 15:47:05 -08:00
export function getExistingValues(token: TemplateToken | null, parent: TemplateToken) {
2022-11-08 17:00:59 -08:00
// For incomplete YAML, we may only have a parent token
if (token) {
2022-11-23 10:54:15 -08:00
if (!isString(token)) {
2022-11-08 17:00:59 -08:00
return;
}
2022-11-23 10:54:15 -08:00
if (isSequence(parent)) {
const sequenceValues = new Set<string>();
2022-11-23 15:35:34 -08:00
2022-11-23 10:54:15 -08:00
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);
}
2022-11-08 17:00:59 -08:00
}
2022-11-23 15:35:34 -08:00
2022-11-23 10:54:15 -08:00
return sequenceValues;
2022-11-08 17:00:59 -08:00
}
}
2022-11-15 13:03:44 -08:00
if (parent.templateTokenType === TokenType.Mapping) {
2022-11-08 17:00:59 -08:00
// No token and parent is a mapping, so we're completing a key
const mapKeys = new Set<string>();
const mapToken = parent as MappingToken;
2022-11-23 15:35:34 -08:00
2022-11-08 17:00:59 -08:00
for (let i = 0; i < mapToken.count; i++) {
const key = mapToken.get(i).key;
2022-11-23 15:35:34 -08:00
if (isString(key)) {
2022-11-15 13:03:44 -08:00
mapKeys.add(key.value);
2022-11-08 17:00:59 -08:00
}
}
return mapKeys;
}
}
2022-11-15 14:08:12 -05:00
function filterAndSortCompletionOptions(options: Value[], existingValues?: Set<string>) {
2022-11-23 15:35:34 -08:00
options = options.filter(x => !existingValues?.has(x.label));
2022-11-08 17:00:59 -08:00
options.sort((a, b) => a.label.localeCompare(b.label));
return options;
}