Initial code import
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* @github/c2c-actions-experience-parser-reviewers
|
||||
@@ -0,0 +1,25 @@
|
||||
name: Build & Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: 'npm'
|
||||
registry-url: 'https://npm.pkg.github.com'
|
||||
- run: npm ci
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- run: npm run build --if-present
|
||||
- run: npm test
|
||||
@@ -0,0 +1,25 @@
|
||||
name: Publish npm package
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
registry-url: 'https://npm.pkg.github.com'
|
||||
scope: '@github'
|
||||
|
||||
- run: npm ci
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish packages
|
||||
run: npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,5 @@
|
||||
*/node_modules
|
||||
*/dist
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"name": "vscode-jest-tests.v2",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--runInBand",
|
||||
"--watchAll=false",
|
||||
"--testNamePattern",
|
||||
"${jest.testNamePattern}",
|
||||
"--runTestsByPath",
|
||||
"${jest.testFile}"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--experimental-vm-modules"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"files.exclude": {
|
||||
"**/.git": true,
|
||||
"**/.svn": true,
|
||||
"**/.hg": true,
|
||||
"**/CVS": true,
|
||||
"**/.DS_Store": true,
|
||||
"**/Thumbs.db": true,
|
||||
// "dist": true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
This wraps the GitHub Actions language service and makes it available via the [language server protocol](https://microsoft.github.io/language-server-protocol/) (LSP).
|
||||
@@ -0,0 +1,16 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
export default {
|
||||
preset: "ts-jest/presets/default-esm",
|
||||
moduleNameMapper: {
|
||||
"^(\\.{1,2}/.*)\\.js$": "$1",
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.tsx?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
useESM: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
moduleFileExtensions: ["ts", "js"],
|
||||
};
|
||||
@@ -36,7 +36,7 @@
|
||||
"watch": "tsc --build tsconfig.build.json --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/actions-languageservice": "^0.1.0",
|
||||
"@github/actions-languageservice": "*",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"vscode-languageserver": "^8.0.2",
|
||||
"vscode-languageserver-textdocument": "^1.0.7"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { validate } from "@github/actions-languageservice";
|
||||
import { TextDocument } from "vscode-languageserver-textdocument";
|
||||
|
||||
describe("simple test", () => {
|
||||
it("should work", async () => {
|
||||
const doc = TextDocument.create("uri", "workflow", 1, "on: push");
|
||||
|
||||
const r = await validate(doc);
|
||||
expect(r).not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
import {
|
||||
CompletionItem,
|
||||
createConnection,
|
||||
Hover,
|
||||
HoverParams,
|
||||
InitializeParams,
|
||||
InitializeResult,
|
||||
ProposedFeatures,
|
||||
TextDocumentPositionParams,
|
||||
TextDocuments,
|
||||
TextDocumentSyncKind,
|
||||
} from "vscode-languageserver/node";
|
||||
|
||||
import { hover, validate } from "@github/actions-languageservice";
|
||||
import { TextDocument } from "vscode-languageserver-textdocument";
|
||||
import {
|
||||
InitializationOptions,
|
||||
RepositoryContext,
|
||||
} from "./initializationOptions";
|
||||
import { onCompletion } from "./on-completion";
|
||||
|
||||
// Create a connection for the server, using Node's IPC as a transport.
|
||||
// Also include all preview / proposed LSP features.
|
||||
const connection = createConnection(ProposedFeatures.all);
|
||||
|
||||
// Create a simple text document manager.
|
||||
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
|
||||
|
||||
let sessionToken: string | undefined;
|
||||
let repos: RepositoryContext[] = [];
|
||||
let repoWorkspaceMap = new Map<string, RepositoryContext>();
|
||||
|
||||
let hasConfigurationCapability = false;
|
||||
let hasWorkspaceFolderCapability = false;
|
||||
let hasDiagnosticRelatedInformationCapability = false;
|
||||
|
||||
connection.onInitialize((params: InitializeParams) => {
|
||||
const capabilities = params.capabilities;
|
||||
|
||||
hasWorkspaceFolderCapability = !!(
|
||||
capabilities.workspace && !!capabilities.workspace.workspaceFolders
|
||||
);
|
||||
hasDiagnosticRelatedInformationCapability = !!(
|
||||
capabilities.textDocument &&
|
||||
capabilities.textDocument.publishDiagnostics &&
|
||||
capabilities.textDocument.publishDiagnostics.relatedInformation
|
||||
);
|
||||
|
||||
const options: InitializationOptions = params.initializationOptions;
|
||||
sessionToken = options.sessionToken;
|
||||
if (options.repos) {
|
||||
repos = options.repos;
|
||||
for (const repo of repos) {
|
||||
repoWorkspaceMap.set(repo.workspaceUri, repo);
|
||||
}
|
||||
}
|
||||
|
||||
const result: InitializeResult = {
|
||||
capabilities: {
|
||||
textDocumentSync: TextDocumentSyncKind.Incremental,
|
||||
completionProvider: {
|
||||
resolveProvider: false,
|
||||
},
|
||||
hoverProvider: true,
|
||||
},
|
||||
};
|
||||
if (hasWorkspaceFolderCapability) {
|
||||
result.capabilities.workspace = {
|
||||
workspaceFolders: {
|
||||
supported: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
connection.onInitialized(() => {
|
||||
if (hasWorkspaceFolderCapability) {
|
||||
connection.workspace.onDidChangeWorkspaceFolders((_event) => {
|
||||
connection.console.log("Workspace folder change event received.");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// The content of a text document has changed. This event is emitted
|
||||
// when the text document first opened or when its content has changed.
|
||||
documents.onDidChangeContent((change) => {
|
||||
validateTextDocument(change.document);
|
||||
});
|
||||
|
||||
async function validateTextDocument(textDocument: TextDocument): Promise<void> {
|
||||
const result = await validate(textDocument);
|
||||
|
||||
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: result });
|
||||
}
|
||||
|
||||
connection.onDidChangeWatchedFiles((_change) => {
|
||||
// Monitored files have change in VSCode
|
||||
connection.console.log("We received an file change event");
|
||||
});
|
||||
|
||||
// This handler provides the initial list of the completion items.
|
||||
connection.onCompletion(
|
||||
async ({
|
||||
position,
|
||||
textDocument,
|
||||
}: TextDocumentPositionParams): Promise<CompletionItem[]> => {
|
||||
return await onCompletion(
|
||||
position,
|
||||
textDocument.uri,
|
||||
documents.get(textDocument.uri)!,
|
||||
sessionToken,
|
||||
repoWorkspaceMap
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
connection.onHover(
|
||||
async ({ position, textDocument }: HoverParams): Promise<Hover | null> => {
|
||||
const r = await hover(documents.get(textDocument.uri)!, position);
|
||||
return r;
|
||||
}
|
||||
);
|
||||
|
||||
// Make the text document manager listen on the connection
|
||||
// for open, change and close text document events
|
||||
documents.listen(connection);
|
||||
|
||||
// Listen on the connection
|
||||
connection.listen();
|
||||
@@ -0,0 +1,12 @@
|
||||
export interface InitializationOptions {
|
||||
sessionToken?: string;
|
||||
repos?: RepositoryContext[];
|
||||
}
|
||||
|
||||
export interface RepositoryContext {
|
||||
id: number;
|
||||
owner: string;
|
||||
name: string;
|
||||
|
||||
workspaceUri: string;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { complete } from "@github/actions-languageservice/complete";
|
||||
import {
|
||||
ValueProviderConfig,
|
||||
WorkflowContext,
|
||||
} from "@github/actions-languageservice/value-providers/config";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import { CompletionItem, DocumentUri, Position } from "vscode-languageserver";
|
||||
import { TextDocument } from "vscode-languageserver-textdocument";
|
||||
import { RepositoryContext } from "./initializationOptions";
|
||||
import { getRunnerLabels } from "./value-providers/runs-on";
|
||||
|
||||
export async function onCompletion(
|
||||
position: Position,
|
||||
uri: DocumentUri,
|
||||
document: TextDocument,
|
||||
sessionToken: string | undefined,
|
||||
repoWorkflowMap: Map<string, RepositoryContext>
|
||||
): Promise<CompletionItem[]> {
|
||||
const config: ValueProviderConfig = {
|
||||
getCustomValues: async (key: string, context: WorkflowContext) =>
|
||||
getCustomValues(key, context, sessionToken, repoWorkflowMap),
|
||||
};
|
||||
return await complete(document, position, config);
|
||||
}
|
||||
|
||||
async function getCustomValues(
|
||||
key: string,
|
||||
context: WorkflowContext,
|
||||
sessionToken: string | undefined,
|
||||
repoWorkspaceMap: Map<string, RepositoryContext>
|
||||
) {
|
||||
if (!sessionToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Parse workflow URI and look up repo for workspace
|
||||
const [repo] = repoWorkspaceMap.values();
|
||||
if (!repo) {
|
||||
return;
|
||||
}
|
||||
if (key === "runs-on") {
|
||||
const octokit = new Octokit({
|
||||
auth: sessionToken,
|
||||
});
|
||||
return await getRunnerLabels(octokit, repo.owner, repo.name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Value } from "@github/actions-languageservice/value-providers/config";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
|
||||
export async function getRunnerLabels(
|
||||
client: Octokit,
|
||||
owner: string,
|
||||
name: string
|
||||
): Promise<Value[]> {
|
||||
const labels = new Set<string>([
|
||||
"ubuntu-22.04",
|
||||
"ubuntu-latest",
|
||||
"ubuntu-20.04",
|
||||
"ubuntu-18.04",
|
||||
"windows-latest",
|
||||
"windows-2022",
|
||||
"windows-2019",
|
||||
"windows-2016",
|
||||
"macos-latest",
|
||||
"macos-12",
|
||||
"macos-11",
|
||||
"macos-10.15",
|
||||
"self-hosted",
|
||||
]);
|
||||
|
||||
try {
|
||||
const response = await client.actions.listSelfHostedRunnersForRepo({
|
||||
owner,
|
||||
repo: name,
|
||||
});
|
||||
|
||||
for (const runner of response.data.runners) {
|
||||
for (const label of runner.labels) {
|
||||
labels.add(label.name);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
return Array.from(labels).map((label) => ({ label }));
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"exclude": ["./src/**/*.test.ts"],
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"noEmit": false,
|
||||
"outDir": "./dist"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"target": "ES2020",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"watchOptions": {
|
||||
"watchFile": "useFsEvents",
|
||||
"watchDirectory": "useFsEvents",
|
||||
"synchronousWatchDirectory": true,
|
||||
"excludeDirectories": ["**/node_modules", "dist"],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"printWidth": 120,
|
||||
"singleQuote": false,
|
||||
"bracketSpacing": false,
|
||||
"trailingComma": "none",
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
This contains the logic for the GitHub Actions workflows language server.
|
||||
@@ -0,0 +1,16 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
export default {
|
||||
preset: "ts-jest/presets/default-esm",
|
||||
moduleNameMapper: {
|
||||
"^(\\.{1,2}/.*)\\.js$": "$1",
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.tsx?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
useESM: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
moduleFileExtensions: ["ts", "js"]
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
import { complete } from "./complete";
|
||||
import { getPositionFromCursor } from "./test-utils/cursor-position";
|
||||
import {
|
||||
Value,
|
||||
ValueProviderConfig,
|
||||
WorkflowContext,
|
||||
} from "./value-providers/config";
|
||||
|
||||
describe("completion", () => {
|
||||
it("runs-on", async () => {
|
||||
const input = "on: push\njobs:\n build:\n runs-on: |";
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result.length).toEqual(11);
|
||||
expect(result[0].label).toEqual("macos-10.13");
|
||||
});
|
||||
|
||||
it("empty workflow", async () => {
|
||||
const input = "|";
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result.length).toEqual(8);
|
||||
expect(result[0].label).toEqual("concurrency");
|
||||
});
|
||||
|
||||
it("completion within a sequence", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted, u|]`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result.length).toEqual(10);
|
||||
|
||||
expect(result[0].label).toEqual("macos-10.13");
|
||||
});
|
||||
|
||||
it("sequence filters out existing values", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [ubuntu-latest, u|]`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result.length).toEqual(10);
|
||||
|
||||
expect(result[0].label).toEqual("macos-10.13");
|
||||
});
|
||||
|
||||
it("one-of definition completion", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
|`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result.length).toEqual(20);
|
||||
});
|
||||
|
||||
it("map keys filter out existing values", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result.map((x) => x.label)).not.toContain("runs-on");
|
||||
});
|
||||
|
||||
it("one-of narrows down to a specific type", async () => {
|
||||
// A job could be a job-factory or a workflow-job (callable workflow with uses)
|
||||
// If we have `runs-on`, we should be able to identify that we're in a job-factory
|
||||
const jobFactory = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|`;
|
||||
const jobFactoryResult = await complete(
|
||||
...getPositionFromCursor(jobFactory)
|
||||
);
|
||||
expect(jobFactoryResult).not.toBeUndefined();
|
||||
expect(jobFactoryResult.map((x) => x.label)).not.toContain("uses");
|
||||
|
||||
const workflowJob = `on: push
|
||||
jobs:
|
||||
build:
|
||||
uses: octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89
|
||||
|`;
|
||||
const workflowJobResult = await complete(
|
||||
...getPositionFromCursor(workflowJob)
|
||||
);
|
||||
expect(workflowJobResult).not.toBeUndefined();
|
||||
expect(workflowJobResult.map((x) => x.label)).not.toContain("runs-on");
|
||||
});
|
||||
|
||||
it("completes boolean values", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
continue-on-error: t|`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result.length).toEqual(2);
|
||||
|
||||
expect(result.map((x) => x.label).sort()).toEqual(["false", "true"]);
|
||||
});
|
||||
|
||||
it("completes for empty map values", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
continue-on-error: |`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result.length).toEqual(2);
|
||||
|
||||
expect(result.map((x) => x.label).sort()).toEqual(["false", "true"]);
|
||||
});
|
||||
|
||||
it("does not complete empty map values when cursor is immediately after the position", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
continue-on-error:|`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("custom value providers override defaults", async () => {
|
||||
const input = "on: push\njobs:\n build:\n runs-on: |";
|
||||
|
||||
const getCustomValues = async (
|
||||
key: string,
|
||||
_: WorkflowContext
|
||||
): Promise<Value[] | undefined> => {
|
||||
if (key === "runs-on") {
|
||||
return [{ label: "my-custom-label" }];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
const config: ValueProviderConfig = {
|
||||
getCustomValues: getCustomValues,
|
||||
};
|
||||
const result = await complete(...getPositionFromCursor(input), config);
|
||||
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result.length).toEqual(1);
|
||||
expect(result[0].label).toEqual("my-custom-label");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
import { parseWorkflow } from "@github/actions-workflow-parser";
|
||||
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);
|
||||
|
||||
const [innerToken, parent] = findInnerTokenAndParent(newPos, result.value);
|
||||
const values = await getValues(
|
||||
innerToken,
|
||||
parent,
|
||||
newPos,
|
||||
textDocument.uri,
|
||||
valueProviderConfig
|
||||
);
|
||||
return values.map((value) => CompletionItem.create(value.label));
|
||||
}
|
||||
|
||||
async function getValues(
|
||||
token: TemplateToken | null,
|
||||
parent: TemplateToken | null,
|
||||
position: Position,
|
||||
workflowUri: string,
|
||||
valueProviderConfig: ValueProviderConfig | undefined
|
||||
): 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,
|
||||
{ uri: workflowUri }
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
describe("validation", () => {
|
||||
it("valid workflow", async () => {
|
||||
// const result = await hover(null as any, {
|
||||
// line: 1,
|
||||
// character: 2,
|
||||
// });
|
||||
// expect(result).not.toBeUndefined();
|
||||
// expect(result?.contents).toEqual(
|
||||
// "The name of the GitHub event that triggers the workflow. You can provide a single event string, array of events, array of event types, or an event configuration map that schedules a workflow or restricts the execution of a workflow to specific files, tags, or branch changes. For a list of available events, see https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows."
|
||||
// );
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
parseWorkflow,
|
||||
ParseWorkflowResult,
|
||||
} from "@github/actions-workflow-parser";
|
||||
import { TemplateToken } from "@github/actions-workflow-parser/templates/tokens/template-token";
|
||||
import { MappingToken } from "@github/actions-workflow-parser/templates/tokens/mapping-token";
|
||||
import { SequenceToken } from "@github/actions-workflow-parser/templates/tokens/sequence-token";
|
||||
import { File } from "@github/actions-workflow-parser/workflows/file";
|
||||
import { Position, TextDocument } from "vscode-languageserver-textdocument";
|
||||
import { Hover } from "vscode-languageserver-types";
|
||||
import { nullTrace } from "./nulltrace";
|
||||
import { findInnerToken } from "./utils/find-token";
|
||||
|
||||
export async function hover(
|
||||
document: TextDocument,
|
||||
position: Position
|
||||
): Promise<Hover | null> {
|
||||
const file: File = {
|
||||
name: document.uri,
|
||||
content: document.getText(),
|
||||
};
|
||||
const result = parseWorkflow(file.name, [file], nullTrace);
|
||||
|
||||
// Find inner token returns null if position is not in a token
|
||||
const innerToken = findInnerToken(position, result.value);
|
||||
if (result.value && innerToken) {
|
||||
return getHover(innerToken);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getHover(innerToken: TemplateToken): Hover | null {
|
||||
if (innerToken.definition) {
|
||||
let description = "";
|
||||
if (innerToken.description) {
|
||||
description = innerToken.description;
|
||||
}
|
||||
|
||||
if (innerToken.definition.evaluatorContext.length > 0) {
|
||||
// Only add padding if there is a description
|
||||
description += `${description.length > 0 ? `\n\n` : ""}**Context:** ${
|
||||
innerToken.definition.evaluatorContext.join(", ")
|
||||
}`;
|
||||
}
|
||||
|
||||
return {
|
||||
contents: description,
|
||||
range: {
|
||||
start: {
|
||||
line: innerToken.range!.start[0],
|
||||
character: innerToken.range!.start[1],
|
||||
},
|
||||
end: {
|
||||
line: innerToken.range!.end[0],
|
||||
character: innerToken.range!.end[1],
|
||||
},
|
||||
},
|
||||
} as Hover;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { hover } from "./hover";
|
||||
export { validate } from "./validate";
|
||||
export { complete } from "./complete";
|
||||
@@ -0,0 +1,7 @@
|
||||
import { TraceWriter } from "@github/actions-workflow-parser/templates/trace-writer";
|
||||
|
||||
export const nullTrace: TraceWriter = {
|
||||
info: (x) => {},
|
||||
verbose: (x) => {},
|
||||
error: (x) => {},
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { getPositionFromCursor } from "./cursor-position";
|
||||
|
||||
describe("getPositionFromCursor", () => {
|
||||
it("returns the position of the cursor and the document without that cursor", () => {
|
||||
const input = "on: push\njobs:|";
|
||||
const [newDoc, position] = getPositionFromCursor(input);
|
||||
|
||||
expect(position).toEqual({ line: 1, character: 5 });
|
||||
expect(newDoc.getText()).toEqual("on: push\njobs:");
|
||||
});
|
||||
|
||||
it("throws an error if no cursor is found", () => {
|
||||
const input = "on: push\njobs:";
|
||||
expect(() => getPositionFromCursor(input)).toThrowError(
|
||||
"No cursor found in document"
|
||||
);
|
||||
});
|
||||
|
||||
it("handles a cursor at the beginning of the document", () => {
|
||||
const input = "|on: push\njobs:";
|
||||
const [newDoc, position] = getPositionFromCursor(input);
|
||||
|
||||
expect(position).toEqual({ line: 0, character: 0 });
|
||||
expect(newDoc.getText()).toEqual("on: push\njobs:");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { TextDocument, Position } from "vscode-languageserver-textdocument";
|
||||
|
||||
// Calculates the position of the cursor and the document without that cursor
|
||||
// Cursor is represented by a `|` character
|
||||
export function getPositionFromCursor(input: string): [TextDocument, Position] {
|
||||
const doc = TextDocument.create("test://test/test.yaml", "yaml", 0, input);
|
||||
|
||||
const cursorIndex = doc.getText().indexOf("|");
|
||||
if (cursorIndex === -1) {
|
||||
throw new Error("No cursor found in document");
|
||||
}
|
||||
|
||||
const position = doc.positionAt(cursorIndex);
|
||||
const newDoc = TextDocument.create(
|
||||
doc.uri,
|
||||
doc.languageId,
|
||||
doc.version,
|
||||
doc.getText().replace("|", "")
|
||||
);
|
||||
|
||||
return [newDoc, position];
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
TemplateToken,
|
||||
MAPPING_TYPE,
|
||||
SEQUENCE_TYPE,
|
||||
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 { Position } from "vscode-languageserver-textdocument";
|
||||
|
||||
export function findInnerToken(pos: Position, root?: TemplateToken) {
|
||||
const [innerToken, _] = findInnerTokenAndParent(pos, root);
|
||||
return innerToken;
|
||||
}
|
||||
|
||||
export function findInnerTokenAndParent(
|
||||
pos: Position,
|
||||
root?: TemplateToken
|
||||
): [TemplateToken | null, TemplateToken | null] {
|
||||
if (!root) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
const s = [root];
|
||||
|
||||
let parent: TemplateToken | null = null;
|
||||
|
||||
for (;;) {
|
||||
const token = s.shift();
|
||||
if (!token) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!posInToken(pos, token)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Position is in token, enqueue children if there are any
|
||||
switch (token.templateTokenType) {
|
||||
case MAPPING_TYPE:
|
||||
const mappingToken = token as MappingToken;
|
||||
parent = mappingToken;
|
||||
for (let i = 0; i < mappingToken.count; i++) {
|
||||
const { key, value } = mappingToken.get(i);
|
||||
|
||||
// Null tokens don't have a position, we can only use the line information
|
||||
if (nullNodeOnLine(pos, key, value)) {
|
||||
return [value, key];
|
||||
}
|
||||
|
||||
s.push(value);
|
||||
}
|
||||
continue;
|
||||
|
||||
case SEQUENCE_TYPE:
|
||||
const sequenceToken = token as SequenceToken;
|
||||
parent = sequenceToken;
|
||||
for (let i = 0; i < sequenceToken.count; i++) {
|
||||
s.push(sequenceToken.get(i));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
return [token, parent];
|
||||
}
|
||||
|
||||
return [null, parent];
|
||||
}
|
||||
|
||||
function posInToken(pos: Position, token: TemplateToken): boolean {
|
||||
if (!token.range) {
|
||||
return false;
|
||||
}
|
||||
const r = token.range;
|
||||
|
||||
// TokenRange is one-based, Position is zero-based
|
||||
const tokenLine = pos.line + 1;
|
||||
const tokenChar = pos.character + 1;
|
||||
|
||||
// Check lines
|
||||
if (r.start[0] > tokenLine || tokenLine > r.end[0]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Position is within the token lines. Check character/column if pos line matches
|
||||
// start or end
|
||||
if (
|
||||
(r.start[0] === tokenLine && tokenChar < r.start[1]) ||
|
||||
(r.end[0] === tokenLine && tokenChar > r.end[1])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function nullNodeOnLine(
|
||||
pos: Position,
|
||||
key: TemplateToken,
|
||||
value: TemplateToken
|
||||
): boolean {
|
||||
if (value.templateTokenType !== NULL_TYPE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!value.range) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!key.range) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value.range.start[0] !== value.range.end[0]) {
|
||||
// Token occupies multiple lines, can't be a null node
|
||||
return false;
|
||||
}
|
||||
|
||||
// TokenRange is one-based, Position is zero-based
|
||||
const posLine = pos.line + 1;
|
||||
if (posLine != value.range.start[0]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Position, TextDocument } from "vscode-languageserver-textdocument";
|
||||
|
||||
const DUMMY_KEY = "dummy";
|
||||
|
||||
// Transform a document to work around YAML parsing issues
|
||||
// Based on `_transform` in https://github.com/cschleiden/github-actions-parser/blob/main/src/lib/parser/complete.ts#L311
|
||||
export function transform(
|
||||
doc: TextDocument,
|
||||
pos: Position
|
||||
): [TextDocument, Position] {
|
||||
const input = doc.getText();
|
||||
let offset = doc.offsetAt(pos);
|
||||
// TODO: Optimize this...
|
||||
const lines = input.split("\n");
|
||||
const lineNo = input
|
||||
.substring(0, offset)
|
||||
.split("")
|
||||
.filter((x) => x === "\n").length;
|
||||
const linePos =
|
||||
offset - lines.slice(0, lineNo).reduce((p, l) => p + l.length + 1, 0);
|
||||
const line = lines[lineNo];
|
||||
|
||||
let partialInput = line.trim();
|
||||
// Special case for Actions, if this line contains an expression marker, do _not_ transform. This is
|
||||
// an ugly fix for auto-completion in multi-line YAML strings. At this point in the process, we cannot
|
||||
// determine if a line is in such a multi-line string.
|
||||
if (partialInput.indexOf("${{") === -1) {
|
||||
const colon = line.indexOf(":");
|
||||
if (colon === -1) {
|
||||
const trimmedLine = line.trim();
|
||||
if (trimmedLine === "" || trimmedLine === "-") {
|
||||
// Node in sequence or empty line
|
||||
let spacer = "";
|
||||
if (trimmedLine === "-" && !line.endsWith(" ")) {
|
||||
spacer = " ";
|
||||
offset++;
|
||||
}
|
||||
|
||||
lines[lineNo] =
|
||||
line.substring(0, linePos) +
|
||||
spacer +
|
||||
DUMMY_KEY +
|
||||
(trimmedLine === "-" ? "" : ":") +
|
||||
line.substring(linePos);
|
||||
|
||||
// Adjust pos by one to prevent a sequence node being marked as active
|
||||
offset++;
|
||||
} else if (!trimmedLine.startsWith("-")) {
|
||||
// Add `:` to end of line
|
||||
lines[lineNo] = line + ":";
|
||||
}
|
||||
|
||||
if (trimmedLine.startsWith("-")) {
|
||||
partialInput = trimmedLine
|
||||
.substring(trimmedLine.indexOf("-") + 1)
|
||||
.trim();
|
||||
}
|
||||
} else {
|
||||
partialInput = (
|
||||
offset > colon ? line.substring(colon + 1) : line.substring(0, colon)
|
||||
).trim();
|
||||
offset = offset - 1;
|
||||
}
|
||||
}
|
||||
const newDoc = TextDocument.create(
|
||||
doc.uri,
|
||||
doc.languageId,
|
||||
doc.version,
|
||||
lines.join("\n")
|
||||
);
|
||||
return [newDoc, newDoc.positionAt(offset)];
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { parseWorkflow } from "@github/actions-workflow-parser";
|
||||
import { TemplateValidationError } from "@github/actions-workflow-parser/templates/template-validation-error";
|
||||
import { nullTrace } from "./nulltrace";
|
||||
|
||||
describe("validation", () => {
|
||||
it("valid workflow", () => {
|
||||
const result = parseWorkflow(
|
||||
"wf.yaml",
|
||||
[
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: "on: push\njobs:\n build:\n runs-on: ubuntu-latest",
|
||||
},
|
||||
],
|
||||
nullTrace
|
||||
);
|
||||
|
||||
expect(result.context.errors.getErrors().length).toBe(0);
|
||||
});
|
||||
|
||||
it("missing jobs key", () => {
|
||||
const result = parseWorkflow(
|
||||
"wf.yaml",
|
||||
[
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: "on: push",
|
||||
},
|
||||
],
|
||||
nullTrace
|
||||
);
|
||||
|
||||
expect(result.context.errors.getErrors().length).toBe(1);
|
||||
expect(result.context.errors.getErrors()[0]).toEqual(
|
||||
new TemplateValidationError(
|
||||
"Required property is missing: jobs",
|
||||
"wf.yaml (Line: 1, Col: 1)",
|
||||
undefined,
|
||||
{
|
||||
start: [1, 1],
|
||||
end: [1, 9],
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it("extraneous key", () => {
|
||||
const result = parseWorkflow(
|
||||
"wf.yaml",
|
||||
[
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on: push
|
||||
unknown-key: foo
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo`,
|
||||
},
|
||||
],
|
||||
nullTrace
|
||||
);
|
||||
|
||||
expect(result.context.errors.getErrors().length).toBe(1);
|
||||
expect(result.context.errors.getErrors()[0]).toEqual(
|
||||
new TemplateValidationError(
|
||||
"Unexpected value 'unknown-key'",
|
||||
"wf.yaml (Line: 2, Col: 1)",
|
||||
undefined,
|
||||
{
|
||||
start: [2, 1],
|
||||
end: [2, 12],
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
convertWorkflowTemplate,
|
||||
parseWorkflow,
|
||||
ParseWorkflowResult,
|
||||
} from "@github/actions-workflow-parser";
|
||||
import { TokenRange } from "@github/actions-workflow-parser/templates/tokens/token-range";
|
||||
import { File } from "@github/actions-workflow-parser/workflows/file";
|
||||
import { TextDocument } from "vscode-languageserver-textdocument";
|
||||
import { Diagnostic, Range } from "vscode-languageserver-types";
|
||||
import { nullTrace } from "./nulltrace";
|
||||
|
||||
/**
|
||||
* Validates a workflow file
|
||||
*
|
||||
* @param textDocument Document to validate
|
||||
* @returns Array of diagnostics
|
||||
*/
|
||||
export async function validate(
|
||||
textDocument: TextDocument
|
||||
// TODO: Support multiple files, context for API calls
|
||||
): Promise<Diagnostic[]> {
|
||||
const file: File = {
|
||||
name: textDocument.uri,
|
||||
content: textDocument.getText(),
|
||||
};
|
||||
|
||||
try {
|
||||
const result: ParseWorkflowResult = parseWorkflow(
|
||||
file.name,
|
||||
[file],
|
||||
nullTrace
|
||||
);
|
||||
|
||||
if (result.value) {
|
||||
// Errors will be updated in the context
|
||||
convertWorkflowTemplate(result.context, result.value);
|
||||
}
|
||||
|
||||
// For now map parser errors directly to diagnostics
|
||||
return result.context.errors.getErrors().map((error) => {
|
||||
let range = mapRange(error.range);
|
||||
if (!range) {
|
||||
// Use default range
|
||||
range = {
|
||||
start: {
|
||||
line: 1,
|
||||
character: 1,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
character: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message: error.rawMessage,
|
||||
range,
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
// TODO: Handle error here
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function mapRange(range: TokenRange | undefined): Range | undefined {
|
||||
if (!range) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
start: {
|
||||
line: range.start[0] - 1,
|
||||
character: range.start[1] - 1,
|
||||
},
|
||||
end: {
|
||||
line: range.end[0] - 1,
|
||||
character: range.end[1] - 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export interface Value {
|
||||
label: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export type ValueProvider = () => Value[];
|
||||
|
||||
export interface WorkflowContext {
|
||||
uri: string;
|
||||
}
|
||||
export interface ValueProviderConfig {
|
||||
getCustomValues: (
|
||||
key: string,
|
||||
context: WorkflowContext
|
||||
) => Promise<Value[] | undefined>;
|
||||
getActionInputs?: (
|
||||
owner: string,
|
||||
name: string,
|
||||
ref: string,
|
||||
path?: string
|
||||
) => Promise<ActionInput[]>;
|
||||
}
|
||||
|
||||
export interface ActionInput {
|
||||
name: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Definition } from "@github/actions-workflow-parser/templates/schema/definition";
|
||||
import { BooleanDefinition } from "@github/actions-workflow-parser/templates/schema/boolean-definition";
|
||||
import { MappingDefinition } from "@github/actions-workflow-parser/templates/schema/mapping-definition";
|
||||
import { OneOfDefinition } from "@github/actions-workflow-parser/templates/schema/one-of-definition";
|
||||
import { getWorkflowSchema } from "@github/actions-workflow-parser/workflows/workflow-schema";
|
||||
import { Value, ValueProvider } from "./config";
|
||||
|
||||
export function defaultValueProviders(): { [key: string]: ValueProvider } {
|
||||
const schema = getWorkflowSchema();
|
||||
|
||||
const map: { [key: string]: ValueProvider } = {};
|
||||
for (const key of Object.keys(schema.definitions)) {
|
||||
const provider = definitionValueProvider(key, schema.definitions);
|
||||
if (provider) {
|
||||
map[key] = provider;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...map,
|
||||
"runs-on": () =>
|
||||
stringsToValues([
|
||||
"ubuntu-latest",
|
||||
"ubuntu-18.04",
|
||||
"ubuntu-16.04",
|
||||
"windows-latest",
|
||||
"windows-2019",
|
||||
"windows-2016",
|
||||
"macos-latest",
|
||||
"macos-10.15",
|
||||
"macos-10.14",
|
||||
"macos-10.13",
|
||||
"self-hosted",
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
function definitionValueProvider(
|
||||
key: string,
|
||||
definitions: { [key: string]: Definition }
|
||||
): ValueProvider | undefined {
|
||||
const def = definitions[key];
|
||||
if (def instanceof MappingDefinition) {
|
||||
return mappingValueProvider(def);
|
||||
} else if (def instanceof OneOfDefinition) {
|
||||
return oneOfValueProvider(def, definitions);
|
||||
} else if (def instanceof BooleanDefinition) {
|
||||
return () => stringsToValues(["true", "false"]);
|
||||
}
|
||||
}
|
||||
|
||||
function mappingValueProvider(
|
||||
mappingDefinition: MappingDefinition
|
||||
): ValueProvider {
|
||||
const properties: Value[] = [];
|
||||
for (const [key, value] of Object.entries(mappingDefinition.properties)) {
|
||||
properties.push({ label: key, description: value.description });
|
||||
}
|
||||
return () => properties;
|
||||
}
|
||||
|
||||
function oneOfValueProvider(
|
||||
oneOfDefinition: OneOfDefinition,
|
||||
definitions: { [key: string]: Definition }
|
||||
): ValueProvider {
|
||||
return () => {
|
||||
const values: Value[] = [];
|
||||
for (const key of oneOfDefinition.oneOf) {
|
||||
const provider = definitionValueProvider(key, definitions);
|
||||
if (!provider) {
|
||||
continue;
|
||||
}
|
||||
for (const prop of provider()) {
|
||||
values.push(prop);
|
||||
}
|
||||
}
|
||||
return distinctValues(values);
|
||||
};
|
||||
}
|
||||
|
||||
function stringsToValues(labels: string[]): Value[] {
|
||||
return labels.map((x) => ({ label: x }));
|
||||
}
|
||||
|
||||
function distinctValues(values: Value[]): Value[] {
|
||||
const map = new Map<string, Value>();
|
||||
for (const value of values) {
|
||||
map.set(value.label, value);
|
||||
}
|
||||
return Array.from(map.values());
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"exclude": ["./src/**/*.test.ts", "./src/test-utils"],
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"noEmit": false,
|
||||
"outDir": "./dist"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"target": "ES2020",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"watchOptions": {
|
||||
"watchFile": "useFsEvents",
|
||||
"watchDirectory": "useFsEvents",
|
||||
"synchronousWatchDirectory": true,
|
||||
"excludeDirectories": ["**/node_modules", "dist"],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"actions-languageservice",
|
||||
"actions-languageserver"
|
||||
],
|
||||
"devDependencies": {
|
||||
"lerna": "^6.0.3"
|
||||
},
|
||||
"name": "actions-languageservices"
|
||||
}
|
||||
Reference in New Issue
Block a user