Merge pull request #3 from github/joshmgross/prettier
Run prettier on language service
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.md
|
||||||
|
*.js
|
||||||
|
*.json
|
||||||
@@ -33,7 +33,9 @@
|
|||||||
"prepublishOnly": "npm run build && npm run test",
|
"prepublishOnly": "npm run build && npm run test",
|
||||||
"test": "NODE_OPTIONS=\"--experimental-vm-modules\" jest",
|
"test": "NODE_OPTIONS=\"--experimental-vm-modules\" jest",
|
||||||
"test-watch": "NODE_OPTIONS=\"--experimental-vm-modules\" jest --watch",
|
"test-watch": "NODE_OPTIONS=\"--experimental-vm-modules\" jest --watch",
|
||||||
"watch": "tsc --build tsconfig.build.json --watch"
|
"watch": "tsc --build tsconfig.build.json --watch",
|
||||||
|
"prettier": "prettier .",
|
||||||
|
"prettier-fix": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@github/actions-workflow-parser": "*",
|
"@github/actions-workflow-parser": "*",
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { complete } from "./complete";
|
import {complete} from "./complete";
|
||||||
import { getPositionFromCursor } from "./test-utils/cursor-position";
|
import {getPositionFromCursor} from "./test-utils/cursor-position";
|
||||||
import {
|
import {Value, ValueProviderConfig, WorkflowContext} from "./value-providers/config";
|
||||||
Value,
|
|
||||||
ValueProviderConfig,
|
|
||||||
WorkflowContext,
|
|
||||||
} from "./value-providers/config";
|
|
||||||
|
|
||||||
describe("completion", () => {
|
describe("completion", () => {
|
||||||
it("runs-on", async () => {
|
it("runs-on", async () => {
|
||||||
@@ -69,7 +65,7 @@ jobs:
|
|||||||
|`;
|
|`;
|
||||||
const result = await complete(...getPositionFromCursor(input));
|
const result = await complete(...getPositionFromCursor(input));
|
||||||
expect(result).not.toBeUndefined();
|
expect(result).not.toBeUndefined();
|
||||||
expect(result.map((x) => x.label)).not.toContain("runs-on");
|
expect(result.map(x => x.label)).not.toContain("runs-on");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("one-of narrows down to a specific type", async () => {
|
it("one-of narrows down to a specific type", async () => {
|
||||||
@@ -80,22 +76,18 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|`;
|
|`;
|
||||||
const jobFactoryResult = await complete(
|
const jobFactoryResult = await complete(...getPositionFromCursor(jobFactory));
|
||||||
...getPositionFromCursor(jobFactory)
|
|
||||||
);
|
|
||||||
expect(jobFactoryResult).not.toBeUndefined();
|
expect(jobFactoryResult).not.toBeUndefined();
|
||||||
expect(jobFactoryResult.map((x) => x.label)).not.toContain("uses");
|
expect(jobFactoryResult.map(x => x.label)).not.toContain("uses");
|
||||||
|
|
||||||
const workflowJob = `on: push
|
const workflowJob = `on: push
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
uses: octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89
|
uses: octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89
|
||||||
|`;
|
|`;
|
||||||
const workflowJobResult = await complete(
|
const workflowJobResult = await complete(...getPositionFromCursor(workflowJob));
|
||||||
...getPositionFromCursor(workflowJob)
|
|
||||||
);
|
|
||||||
expect(workflowJobResult).not.toBeUndefined();
|
expect(workflowJobResult).not.toBeUndefined();
|
||||||
expect(workflowJobResult.map((x) => x.label)).not.toContain("runs-on");
|
expect(workflowJobResult.map(x => x.label)).not.toContain("runs-on");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("completes boolean values", async () => {
|
it("completes boolean values", async () => {
|
||||||
@@ -108,7 +100,7 @@ jobs:
|
|||||||
expect(result).not.toBeUndefined();
|
expect(result).not.toBeUndefined();
|
||||||
expect(result.length).toEqual(2);
|
expect(result.length).toEqual(2);
|
||||||
|
|
||||||
expect(result.map((x) => x.label).sort()).toEqual(["false", "true"]);
|
expect(result.map(x => x.label).sort()).toEqual(["false", "true"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("completes for empty map values", async () => {
|
it("completes for empty map values", async () => {
|
||||||
@@ -121,7 +113,7 @@ jobs:
|
|||||||
expect(result).not.toBeUndefined();
|
expect(result).not.toBeUndefined();
|
||||||
expect(result.length).toEqual(2);
|
expect(result.length).toEqual(2);
|
||||||
|
|
||||||
expect(result.map((x) => x.label).sort()).toEqual(["false", "true"]);
|
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 () => {
|
it("does not complete empty map values when cursor is immediately after the position", async () => {
|
||||||
@@ -138,17 +130,14 @@ jobs:
|
|||||||
it("custom value providers override defaults", async () => {
|
it("custom value providers override defaults", async () => {
|
||||||
const input = "on: push\njobs:\n build:\n runs-on: |";
|
const input = "on: push\njobs:\n build:\n runs-on: |";
|
||||||
|
|
||||||
const getCustomValues = async (
|
const getCustomValues = async (key: string, _: WorkflowContext): Promise<Value[] | undefined> => {
|
||||||
key: string,
|
|
||||||
_: WorkflowContext
|
|
||||||
): Promise<Value[] | undefined> => {
|
|
||||||
if (key === "runs-on") {
|
if (key === "runs-on") {
|
||||||
return [{ label: "my-custom-label" }];
|
return [{label: "my-custom-label"}];
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
const config: ValueProviderConfig = {
|
const config: ValueProviderConfig = {
|
||||||
getCustomValues: getCustomValues,
|
getCustomValues: getCustomValues
|
||||||
};
|
};
|
||||||
const result = await complete(...getPositionFromCursor(input), config);
|
const result = await complete(...getPositionFromCursor(input), config);
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import { parseWorkflow } from "@github/actions-workflow-parser";
|
import {parseWorkflow} from "@github/actions-workflow-parser";
|
||||||
import {
|
import {
|
||||||
SEQUENCE_TYPE,
|
SEQUENCE_TYPE,
|
||||||
STRING_TYPE,
|
STRING_TYPE,
|
||||||
MAPPING_TYPE,
|
MAPPING_TYPE,
|
||||||
TemplateToken,
|
TemplateToken,
|
||||||
NULL_TYPE,
|
NULL_TYPE
|
||||||
} from "@github/actions-workflow-parser/templates/tokens/index";
|
} 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";
|
||||||
import { SequenceToken } from "@github/actions-workflow-parser/templates/tokens/sequence-token";
|
import {SequenceToken} from "@github/actions-workflow-parser/templates/tokens/sequence-token";
|
||||||
import { StringToken } from "@github/actions-workflow-parser/templates/tokens/string-token";
|
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
|
||||||
import { File } from "@github/actions-workflow-parser/workflows/file";
|
import {File} from "@github/actions-workflow-parser/workflows/file";
|
||||||
import { Position, TextDocument } from "vscode-languageserver-textdocument";
|
import {Position, TextDocument} from "vscode-languageserver-textdocument";
|
||||||
import { CompletionItem } from "vscode-languageserver-types";
|
import {CompletionItem} from "vscode-languageserver-types";
|
||||||
import { nullTrace } from "./nulltrace";
|
import {nullTrace} from "./nulltrace";
|
||||||
import { findInnerTokenAndParent } from "./utils/find-token";
|
import {findInnerTokenAndParent} from "./utils/find-token";
|
||||||
import { transform } from "./utils/transform";
|
import {transform} from "./utils/transform";
|
||||||
import { Value, ValueProviderConfig } from "./value-providers/config";
|
import {Value, ValueProviderConfig} from "./value-providers/config";
|
||||||
import { defaultValueProviders } from "./value-providers/default";
|
import {defaultValueProviders} from "./value-providers/default";
|
||||||
|
|
||||||
export async function complete(
|
export async function complete(
|
||||||
textDocument: TextDocument,
|
textDocument: TextDocument,
|
||||||
@@ -28,19 +28,13 @@ export async function complete(
|
|||||||
|
|
||||||
const file: File = {
|
const file: File = {
|
||||||
name: textDocument.uri,
|
name: textDocument.uri,
|
||||||
content: newDoc.getText(),
|
content: newDoc.getText()
|
||||||
};
|
};
|
||||||
const result = parseWorkflow(file.name, [file], nullTrace);
|
const result = parseWorkflow(file.name, [file], nullTrace);
|
||||||
|
|
||||||
const [innerToken, parent] = findInnerTokenAndParent(newPos, result.value);
|
const [innerToken, parent] = findInnerTokenAndParent(newPos, result.value);
|
||||||
const values = await getValues(
|
const values = await getValues(innerToken, parent, newPos, textDocument.uri, valueProviderConfig);
|
||||||
innerToken,
|
return values.map(value => CompletionItem.create(value.label));
|
||||||
parent,
|
|
||||||
newPos,
|
|
||||||
textDocument.uri,
|
|
||||||
valueProviderConfig
|
|
||||||
);
|
|
||||||
return values.map((value) => CompletionItem.create(value.label));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getValues(
|
async function getValues(
|
||||||
@@ -65,10 +59,7 @@ async function getValues(
|
|||||||
|
|
||||||
let customValues: Value[] | undefined = undefined;
|
let customValues: Value[] | undefined = undefined;
|
||||||
if (token?.definition?.key) {
|
if (token?.definition?.key) {
|
||||||
customValues = await valueProviderConfig?.getCustomValues(
|
customValues = await valueProviderConfig?.getCustomValues(token.definition.key, {uri: workflowUri});
|
||||||
token.definition.key,
|
|
||||||
{ uri: workflowUri }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (customValues !== undefined) {
|
if (customValues !== undefined) {
|
||||||
@@ -93,10 +84,7 @@ async function getValues(
|
|||||||
function getExistingValues(token: TemplateToken | null, parent: TemplateToken) {
|
function getExistingValues(token: TemplateToken | null, parent: TemplateToken) {
|
||||||
// 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 (
|
if (token.templateTokenType !== STRING_TYPE || parent.templateTokenType !== SEQUENCE_TYPE) {
|
||||||
token.templateTokenType !== STRING_TYPE ||
|
|
||||||
parent.templateTokenType !== SEQUENCE_TYPE
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,13 +115,8 @@ function getExistingValues(token: TemplateToken | null, parent: TemplateToken) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterAndSortCompletionOptions(
|
function filterAndSortCompletionOptions(options: Value[], existingValues?: Set<string>) {
|
||||||
options: Value[],
|
options = options.filter(x => !existingValues || !existingValues.has(x.label));
|
||||||
existingValues?: Set<string>
|
|
||||||
) {
|
|
||||||
options = options.filter(
|
|
||||||
(x) => !existingValues || !existingValues.has(x.label)
|
|
||||||
);
|
|
||||||
options.sort((a, b) => a.label.localeCompare(b.label));
|
options.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,17 @@
|
|||||||
import {
|
import {parseWorkflow, ParseWorkflowResult} from "@github/actions-workflow-parser";
|
||||||
parseWorkflow,
|
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
|
||||||
ParseWorkflowResult,
|
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token";
|
||||||
} from "@github/actions-workflow-parser";
|
import {SequenceToken} from "@github/actions-workflow-parser/templates/tokens/sequence-token";
|
||||||
import { TemplateToken } from "@github/actions-workflow-parser/templates/tokens/template-token";
|
import {File} from "@github/actions-workflow-parser/workflows/file";
|
||||||
import { MappingToken } from "@github/actions-workflow-parser/templates/tokens/mapping-token";
|
import {Position, TextDocument} from "vscode-languageserver-textdocument";
|
||||||
import { SequenceToken } from "@github/actions-workflow-parser/templates/tokens/sequence-token";
|
import {Hover} from "vscode-languageserver-types";
|
||||||
import { File } from "@github/actions-workflow-parser/workflows/file";
|
import {nullTrace} from "./nulltrace";
|
||||||
import { Position, TextDocument } from "vscode-languageserver-textdocument";
|
import {findInnerToken} from "./utils/find-token";
|
||||||
import { Hover } from "vscode-languageserver-types";
|
|
||||||
import { nullTrace } from "./nulltrace";
|
|
||||||
import { findInnerToken } from "./utils/find-token";
|
|
||||||
|
|
||||||
export async function hover(
|
export async function hover(document: TextDocument, position: Position): Promise<Hover | null> {
|
||||||
document: TextDocument,
|
|
||||||
position: Position
|
|
||||||
): Promise<Hover | null> {
|
|
||||||
const file: File = {
|
const file: File = {
|
||||||
name: document.uri,
|
name: document.uri,
|
||||||
content: document.getText(),
|
content: document.getText()
|
||||||
};
|
};
|
||||||
const result = parseWorkflow(file.name, [file], nullTrace);
|
const result = parseWorkflow(file.name, [file], nullTrace);
|
||||||
|
|
||||||
@@ -38,9 +32,9 @@ function getHover(innerToken: TemplateToken): Hover | null {
|
|||||||
|
|
||||||
if (innerToken.definition.evaluatorContext.length > 0) {
|
if (innerToken.definition.evaluatorContext.length > 0) {
|
||||||
// Only add padding if there is a description
|
// Only add padding if there is a description
|
||||||
description += `${description.length > 0 ? `\n\n` : ""}**Context:** ${
|
description += `${description.length > 0 ? `\n\n` : ""}**Context:** ${innerToken.definition.evaluatorContext.join(
|
||||||
innerToken.definition.evaluatorContext.join(", ")
|
", "
|
||||||
}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -48,13 +42,13 @@ function getHover(innerToken: TemplateToken): Hover | null {
|
|||||||
range: {
|
range: {
|
||||||
start: {
|
start: {
|
||||||
line: innerToken.range!.start[0],
|
line: innerToken.range!.start[0],
|
||||||
character: innerToken.range!.start[1],
|
character: innerToken.range!.start[1]
|
||||||
},
|
},
|
||||||
end: {
|
end: {
|
||||||
line: innerToken.range!.end[0],
|
line: innerToken.range!.end[0],
|
||||||
character: innerToken.range!.end[1],
|
character: innerToken.range!.end[1]
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
} as Hover;
|
} as Hover;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export { hover } from "./hover";
|
export {hover} from "./hover";
|
||||||
export { validate } from "./validate";
|
export {validate} from "./validate";
|
||||||
export { complete } from "./complete";
|
export {complete} from "./complete";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { TraceWriter } from "@github/actions-workflow-parser/templates/trace-writer";
|
import {TraceWriter} from "@github/actions-workflow-parser/templates/trace-writer";
|
||||||
|
|
||||||
export const nullTrace: TraceWriter = {
|
export const nullTrace: TraceWriter = {
|
||||||
info: (x) => {},
|
info: x => {},
|
||||||
verbose: (x) => {},
|
verbose: x => {},
|
||||||
error: (x) => {},
|
error: x => {}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,26 +1,24 @@
|
|||||||
import { getPositionFromCursor } from "./cursor-position";
|
import {getPositionFromCursor} from "./cursor-position";
|
||||||
|
|
||||||
describe("getPositionFromCursor", () => {
|
describe("getPositionFromCursor", () => {
|
||||||
it("returns the position of the cursor and the document without that cursor", () => {
|
it("returns the position of the cursor and the document without that cursor", () => {
|
||||||
const input = "on: push\njobs:|";
|
const input = "on: push\njobs:|";
|
||||||
const [newDoc, position] = getPositionFromCursor(input);
|
const [newDoc, position] = getPositionFromCursor(input);
|
||||||
|
|
||||||
expect(position).toEqual({ line: 1, character: 5 });
|
expect(position).toEqual({line: 1, character: 5});
|
||||||
expect(newDoc.getText()).toEqual("on: push\njobs:");
|
expect(newDoc.getText()).toEqual("on: push\njobs:");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws an error if no cursor is found", () => {
|
it("throws an error if no cursor is found", () => {
|
||||||
const input = "on: push\njobs:";
|
const input = "on: push\njobs:";
|
||||||
expect(() => getPositionFromCursor(input)).toThrowError(
|
expect(() => getPositionFromCursor(input)).toThrowError("No cursor found in document");
|
||||||
"No cursor found in document"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles a cursor at the beginning of the document", () => {
|
it("handles a cursor at the beginning of the document", () => {
|
||||||
const input = "|on: push\njobs:";
|
const input = "|on: push\njobs:";
|
||||||
const [newDoc, position] = getPositionFromCursor(input);
|
const [newDoc, position] = getPositionFromCursor(input);
|
||||||
|
|
||||||
expect(position).toEqual({ line: 0, character: 0 });
|
expect(position).toEqual({line: 0, character: 0});
|
||||||
expect(newDoc.getText()).toEqual("on: push\njobs:");
|
expect(newDoc.getText()).toEqual("on: push\njobs:");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TextDocument, Position } from "vscode-languageserver-textdocument";
|
import {TextDocument, Position} from "vscode-languageserver-textdocument";
|
||||||
|
|
||||||
// Calculates the position of the cursor and the document without that cursor
|
// Calculates the position of the cursor and the document without that cursor
|
||||||
// Cursor is represented by a `|` character
|
// Cursor is represented by a `|` character
|
||||||
@@ -11,12 +11,7 @@ export function getPositionFromCursor(input: string): [TextDocument, Position] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const position = doc.positionAt(cursorIndex);
|
const position = doc.positionAt(cursorIndex);
|
||||||
const newDoc = TextDocument.create(
|
const newDoc = TextDocument.create(doc.uri, doc.languageId, doc.version, doc.getText().replace("|", ""));
|
||||||
doc.uri,
|
|
||||||
doc.languageId,
|
|
||||||
doc.version,
|
|
||||||
doc.getText().replace("|", "")
|
|
||||||
);
|
|
||||||
|
|
||||||
return [newDoc, position];
|
return [newDoc, position];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import {
|
|||||||
TemplateToken,
|
TemplateToken,
|
||||||
MAPPING_TYPE,
|
MAPPING_TYPE,
|
||||||
SEQUENCE_TYPE,
|
SEQUENCE_TYPE,
|
||||||
NULL_TYPE,
|
NULL_TYPE
|
||||||
} from "@github/actions-workflow-parser/templates/tokens/index";
|
} 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";
|
||||||
import { SequenceToken } from "@github/actions-workflow-parser/templates/tokens/sequence-token";
|
import {SequenceToken} from "@github/actions-workflow-parser/templates/tokens/sequence-token";
|
||||||
import { Position } from "vscode-languageserver-textdocument";
|
import {Position} from "vscode-languageserver-textdocument";
|
||||||
|
|
||||||
export function findInnerToken(pos: Position, root?: TemplateToken) {
|
export function findInnerToken(pos: Position, root?: TemplateToken) {
|
||||||
const [innerToken, _] = findInnerTokenAndParent(pos, root);
|
const [innerToken, _] = findInnerTokenAndParent(pos, root);
|
||||||
@@ -41,7 +41,7 @@ export function findInnerTokenAndParent(
|
|||||||
const mappingToken = token as MappingToken;
|
const mappingToken = token as MappingToken;
|
||||||
parent = mappingToken;
|
parent = mappingToken;
|
||||||
for (let i = 0; i < mappingToken.count; i++) {
|
for (let i = 0; i < mappingToken.count; i++) {
|
||||||
const { key, value } = mappingToken.get(i);
|
const {key, value} = mappingToken.get(i);
|
||||||
|
|
||||||
// Null tokens don't have a position, we can only use the line information
|
// Null tokens don't have a position, we can only use the line information
|
||||||
if (nullNodeOnLine(pos, key, value)) {
|
if (nullNodeOnLine(pos, key, value)) {
|
||||||
@@ -84,21 +84,14 @@ function posInToken(pos: Position, token: TemplateToken): boolean {
|
|||||||
|
|
||||||
// Position is within the token lines. Check character/column if pos line matches
|
// Position is within the token lines. Check character/column if pos line matches
|
||||||
// start or end
|
// start or end
|
||||||
if (
|
if ((r.start[0] === tokenLine && tokenChar < r.start[1]) || (r.end[0] === tokenLine && tokenChar > r.end[1])) {
|
||||||
(r.start[0] === tokenLine && tokenChar < r.start[1]) ||
|
|
||||||
(r.end[0] === tokenLine && tokenChar > r.end[1])
|
|
||||||
) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function nullNodeOnLine(
|
function nullNodeOnLine(pos: Position, key: TemplateToken, value: TemplateToken): boolean {
|
||||||
pos: Position,
|
|
||||||
key: TemplateToken,
|
|
||||||
value: TemplateToken
|
|
||||||
): boolean {
|
|
||||||
if (value.templateTokenType !== NULL_TYPE) {
|
if (value.templateTokenType !== NULL_TYPE) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { Position, TextDocument } from "vscode-languageserver-textdocument";
|
import {Position, TextDocument} from "vscode-languageserver-textdocument";
|
||||||
|
|
||||||
const DUMMY_KEY = "dummy";
|
const DUMMY_KEY = "dummy";
|
||||||
|
|
||||||
// Transform a document to work around YAML parsing issues
|
// 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
|
// Based on `_transform` in https://github.com/cschleiden/github-actions-parser/blob/main/src/lib/parser/complete.ts#L311
|
||||||
export function transform(
|
export function transform(doc: TextDocument, pos: Position): [TextDocument, Position] {
|
||||||
doc: TextDocument,
|
|
||||||
pos: Position
|
|
||||||
): [TextDocument, Position] {
|
|
||||||
const input = doc.getText();
|
const input = doc.getText();
|
||||||
let offset = doc.offsetAt(pos);
|
let offset = doc.offsetAt(pos);
|
||||||
// TODO: Optimize this...
|
// TODO: Optimize this...
|
||||||
@@ -15,9 +12,8 @@ export function transform(
|
|||||||
const lineNo = input
|
const lineNo = input
|
||||||
.substring(0, offset)
|
.substring(0, offset)
|
||||||
.split("")
|
.split("")
|
||||||
.filter((x) => x === "\n").length;
|
.filter(x => x === "\n").length;
|
||||||
const linePos =
|
const linePos = offset - lines.slice(0, lineNo).reduce((p, l) => p + l.length + 1, 0);
|
||||||
offset - lines.slice(0, lineNo).reduce((p, l) => p + l.length + 1, 0);
|
|
||||||
const line = lines[lineNo];
|
const line = lines[lineNo];
|
||||||
|
|
||||||
let partialInput = line.trim();
|
let partialInput = line.trim();
|
||||||
@@ -37,11 +33,7 @@ export function transform(
|
|||||||
}
|
}
|
||||||
|
|
||||||
lines[lineNo] =
|
lines[lineNo] =
|
||||||
line.substring(0, linePos) +
|
line.substring(0, linePos) + spacer + DUMMY_KEY + (trimmedLine === "-" ? "" : ":") + line.substring(linePos);
|
||||||
spacer +
|
|
||||||
DUMMY_KEY +
|
|
||||||
(trimmedLine === "-" ? "" : ":") +
|
|
||||||
line.substring(linePos);
|
|
||||||
|
|
||||||
// Adjust pos by one to prevent a sequence node being marked as active
|
// Adjust pos by one to prevent a sequence node being marked as active
|
||||||
offset++;
|
offset++;
|
||||||
@@ -51,22 +43,13 @@ export function transform(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (trimmedLine.startsWith("-")) {
|
if (trimmedLine.startsWith("-")) {
|
||||||
partialInput = trimmedLine
|
partialInput = trimmedLine.substring(trimmedLine.indexOf("-") + 1).trim();
|
||||||
.substring(trimmedLine.indexOf("-") + 1)
|
|
||||||
.trim();
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
partialInput = (
|
partialInput = (offset > colon ? line.substring(colon + 1) : line.substring(0, colon)).trim();
|
||||||
offset > colon ? line.substring(colon + 1) : line.substring(0, colon)
|
|
||||||
).trim();
|
|
||||||
offset = offset - 1;
|
offset = offset - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const newDoc = TextDocument.create(
|
const newDoc = TextDocument.create(doc.uri, doc.languageId, doc.version, lines.join("\n"));
|
||||||
doc.uri,
|
|
||||||
doc.languageId,
|
|
||||||
doc.version,
|
|
||||||
lines.join("\n")
|
|
||||||
);
|
|
||||||
return [newDoc, newDoc.positionAt(offset)];
|
return [newDoc, newDoc.positionAt(offset)];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { parseWorkflow } from "@github/actions-workflow-parser";
|
import {parseWorkflow} from "@github/actions-workflow-parser";
|
||||||
import { TemplateValidationError } from "@github/actions-workflow-parser/templates/template-validation-error";
|
import {TemplateValidationError} from "@github/actions-workflow-parser/templates/template-validation-error";
|
||||||
import { nullTrace } from "./nulltrace";
|
import {nullTrace} from "./nulltrace";
|
||||||
|
|
||||||
describe("validation", () => {
|
describe("validation", () => {
|
||||||
it("valid workflow", () => {
|
it("valid workflow", () => {
|
||||||
@@ -9,8 +9,8 @@ describe("validation", () => {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: "wf.yaml",
|
name: "wf.yaml",
|
||||||
content: "on: push\njobs:\n build:\n runs-on: ubuntu-latest",
|
content: "on: push\njobs:\n build:\n runs-on: ubuntu-latest"
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
nullTrace
|
nullTrace
|
||||||
);
|
);
|
||||||
@@ -24,23 +24,18 @@ describe("validation", () => {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: "wf.yaml",
|
name: "wf.yaml",
|
||||||
content: "on: push",
|
content: "on: push"
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
nullTrace
|
nullTrace
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.context.errors.getErrors().length).toBe(1);
|
expect(result.context.errors.getErrors().length).toBe(1);
|
||||||
expect(result.context.errors.getErrors()[0]).toEqual(
|
expect(result.context.errors.getErrors()[0]).toEqual(
|
||||||
new TemplateValidationError(
|
new TemplateValidationError("Required property is missing: jobs", "wf.yaml (Line: 1, Col: 1)", undefined, {
|
||||||
"Required property is missing: jobs",
|
start: [1, 1],
|
||||||
"wf.yaml (Line: 1, Col: 1)",
|
end: [1, 9]
|
||||||
undefined,
|
})
|
||||||
{
|
|
||||||
start: [1, 1],
|
|
||||||
end: [1, 9],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -56,23 +51,18 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- run: echo`,
|
- run: echo`
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
nullTrace
|
nullTrace
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.context.errors.getErrors().length).toBe(1);
|
expect(result.context.errors.getErrors().length).toBe(1);
|
||||||
expect(result.context.errors.getErrors()[0]).toEqual(
|
expect(result.context.errors.getErrors()[0]).toEqual(
|
||||||
new TemplateValidationError(
|
new TemplateValidationError("Unexpected value 'unknown-key'", "wf.yaml (Line: 2, Col: 1)", undefined, {
|
||||||
"Unexpected value 'unknown-key'",
|
start: [2, 1],
|
||||||
"wf.yaml (Line: 2, Col: 1)",
|
end: [2, 12]
|
||||||
undefined,
|
})
|
||||||
{
|
|
||||||
start: [2, 1],
|
|
||||||
end: [2, 12],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import {
|
import {convertWorkflowTemplate, parseWorkflow, ParseWorkflowResult} from "@github/actions-workflow-parser";
|
||||||
convertWorkflowTemplate,
|
import {TokenRange} from "@github/actions-workflow-parser/templates/tokens/token-range";
|
||||||
parseWorkflow,
|
import {File} from "@github/actions-workflow-parser/workflows/file";
|
||||||
ParseWorkflowResult,
|
import {TextDocument} from "vscode-languageserver-textdocument";
|
||||||
} from "@github/actions-workflow-parser";
|
import {Diagnostic, Range} from "vscode-languageserver-types";
|
||||||
import { TokenRange } from "@github/actions-workflow-parser/templates/tokens/token-range";
|
import {nullTrace} from "./nulltrace";
|
||||||
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
|
* Validates a workflow file
|
||||||
@@ -21,15 +17,11 @@ export async function validate(
|
|||||||
): Promise<Diagnostic[]> {
|
): Promise<Diagnostic[]> {
|
||||||
const file: File = {
|
const file: File = {
|
||||||
name: textDocument.uri,
|
name: textDocument.uri,
|
||||||
content: textDocument.getText(),
|
content: textDocument.getText()
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result: ParseWorkflowResult = parseWorkflow(
|
const result: ParseWorkflowResult = parseWorkflow(file.name, [file], nullTrace);
|
||||||
file.name,
|
|
||||||
[file],
|
|
||||||
nullTrace
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.value) {
|
if (result.value) {
|
||||||
// Errors will be updated in the context
|
// Errors will be updated in the context
|
||||||
@@ -37,25 +29,25 @@ export async function validate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For now map parser errors directly to diagnostics
|
// For now map parser errors directly to diagnostics
|
||||||
return result.context.errors.getErrors().map((error) => {
|
return result.context.errors.getErrors().map(error => {
|
||||||
let range = mapRange(error.range);
|
let range = mapRange(error.range);
|
||||||
if (!range) {
|
if (!range) {
|
||||||
// Use default range
|
// Use default range
|
||||||
range = {
|
range = {
|
||||||
start: {
|
start: {
|
||||||
line: 1,
|
line: 1,
|
||||||
character: 1,
|
character: 1
|
||||||
},
|
},
|
||||||
end: {
|
end: {
|
||||||
line: 1,
|
line: 1,
|
||||||
character: 1,
|
character: 1
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: error.rawMessage,
|
message: error.rawMessage,
|
||||||
range,
|
range
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -72,11 +64,11 @@ function mapRange(range: TokenRange | undefined): Range | undefined {
|
|||||||
return {
|
return {
|
||||||
start: {
|
start: {
|
||||||
line: range.start[0] - 1,
|
line: range.start[0] - 1,
|
||||||
character: range.start[1] - 1,
|
character: range.start[1] - 1
|
||||||
},
|
},
|
||||||
end: {
|
end: {
|
||||||
line: range.end[0] - 1,
|
line: range.end[0] - 1,
|
||||||
character: range.end[1] - 1,
|
character: range.end[1] - 1
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,16 +9,8 @@ export interface WorkflowContext {
|
|||||||
uri: string;
|
uri: string;
|
||||||
}
|
}
|
||||||
export interface ValueProviderConfig {
|
export interface ValueProviderConfig {
|
||||||
getCustomValues: (
|
getCustomValues: (key: string, context: WorkflowContext) => Promise<Value[] | undefined>;
|
||||||
key: string,
|
getActionInputs?: (owner: string, name: string, ref: string, path?: string) => Promise<ActionInput[]>;
|
||||||
context: WorkflowContext
|
|
||||||
) => Promise<Value[] | undefined>;
|
|
||||||
getActionInputs?: (
|
|
||||||
owner: string,
|
|
||||||
name: string,
|
|
||||||
ref: string,
|
|
||||||
path?: string
|
|
||||||
) => Promise<ActionInput[]>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionInput {
|
export interface ActionInput {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Definition } from "@github/actions-workflow-parser/templates/schema/definition";
|
import {Definition} from "@github/actions-workflow-parser/templates/schema/definition";
|
||||||
import { BooleanDefinition } from "@github/actions-workflow-parser/templates/schema/boolean-definition";
|
import {BooleanDefinition} from "@github/actions-workflow-parser/templates/schema/boolean-definition";
|
||||||
import { MappingDefinition } from "@github/actions-workflow-parser/templates/schema/mapping-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 {OneOfDefinition} from "@github/actions-workflow-parser/templates/schema/one-of-definition";
|
||||||
import { getWorkflowSchema } from "@github/actions-workflow-parser/workflows/workflow-schema";
|
import {getWorkflowSchema} from "@github/actions-workflow-parser/workflows/workflow-schema";
|
||||||
import { Value, ValueProvider } from "./config";
|
import {Value, ValueProvider} from "./config";
|
||||||
|
|
||||||
export function defaultValueProviders(): { [key: string]: ValueProvider } {
|
export function defaultValueProviders(): {[key: string]: ValueProvider} {
|
||||||
const schema = getWorkflowSchema();
|
const schema = getWorkflowSchema();
|
||||||
|
|
||||||
const map: { [key: string]: ValueProvider } = {};
|
const map: {[key: string]: ValueProvider} = {};
|
||||||
for (const key of Object.keys(schema.definitions)) {
|
for (const key of Object.keys(schema.definitions)) {
|
||||||
const provider = definitionValueProvider(key, schema.definitions);
|
const provider = definitionValueProvider(key, schema.definitions);
|
||||||
if (provider) {
|
if (provider) {
|
||||||
@@ -30,15 +30,12 @@ export function defaultValueProviders(): { [key: string]: ValueProvider } {
|
|||||||
"macos-10.15",
|
"macos-10.15",
|
||||||
"macos-10.14",
|
"macos-10.14",
|
||||||
"macos-10.13",
|
"macos-10.13",
|
||||||
"self-hosted",
|
"self-hosted"
|
||||||
]),
|
])
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function definitionValueProvider(
|
function definitionValueProvider(key: string, definitions: {[key: string]: Definition}): ValueProvider | undefined {
|
||||||
key: string,
|
|
||||||
definitions: { [key: string]: Definition }
|
|
||||||
): ValueProvider | undefined {
|
|
||||||
const def = definitions[key];
|
const def = definitions[key];
|
||||||
if (def instanceof MappingDefinition) {
|
if (def instanceof MappingDefinition) {
|
||||||
return mappingValueProvider(def);
|
return mappingValueProvider(def);
|
||||||
@@ -49,20 +46,15 @@ function definitionValueProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mappingValueProvider(
|
function mappingValueProvider(mappingDefinition: MappingDefinition): ValueProvider {
|
||||||
mappingDefinition: MappingDefinition
|
|
||||||
): ValueProvider {
|
|
||||||
const properties: Value[] = [];
|
const properties: Value[] = [];
|
||||||
for (const [key, value] of Object.entries(mappingDefinition.properties)) {
|
for (const [key, value] of Object.entries(mappingDefinition.properties)) {
|
||||||
properties.push({ label: key, description: value.description });
|
properties.push({label: key, description: value.description});
|
||||||
}
|
}
|
||||||
return () => properties;
|
return () => properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
function oneOfValueProvider(
|
function oneOfValueProvider(oneOfDefinition: OneOfDefinition, definitions: {[key: string]: Definition}): ValueProvider {
|
||||||
oneOfDefinition: OneOfDefinition,
|
|
||||||
definitions: { [key: string]: Definition }
|
|
||||||
): ValueProvider {
|
|
||||||
return () => {
|
return () => {
|
||||||
const values: Value[] = [];
|
const values: Value[] = [];
|
||||||
for (const key of oneOfDefinition.oneOf) {
|
for (const key of oneOfDefinition.oneOf) {
|
||||||
@@ -79,7 +71,7 @@ function oneOfValueProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stringsToValues(labels: string[]): Value[] {
|
function stringsToValues(labels: string[]): Value[] {
|
||||||
return labels.map((x) => ({ label: x }));
|
return labels.map(x => ({label: x}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function distinctValues(values: Value[]): Value[] {
|
function distinctValues(values: Value[]): Value[] {
|
||||||
|
|||||||
Reference in New Issue
Block a user