Merge pull request #48 from github/cschleiden/multi-line-expressions
Validation & Auto completion for multi-line expressions
This commit is contained in:
@@ -88,6 +88,64 @@ describe("expressions", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
describe("multi-line strings", () => {
|
||||
it("indented |", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
steps:
|
||||
- run: |
|
||||
first line
|
||||
test \${{ github.| }}
|
||||
test2`;
|
||||
const result = await complete(...getPositionFromCursor(input, 1), undefined, contextProviderConfig);
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
|
||||
it("indented |+", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
steps:
|
||||
- run: |+
|
||||
first line
|
||||
test \${{ github.| }}
|
||||
test2`;
|
||||
const result = await complete(...getPositionFromCursor(input, 1), undefined, contextProviderConfig);
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
|
||||
it("indented >", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
steps:
|
||||
- run: >
|
||||
first line
|
||||
test \${{ github.| }}
|
||||
test2`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
|
||||
it("indented >+", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
steps:
|
||||
- run: >+
|
||||
first line
|
||||
test \${{ github.| }}
|
||||
test2`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
});
|
||||
|
||||
it("nested auto-complete", async () => {
|
||||
const input = "run-name: ${{ github.| }}";
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
|
||||
@@ -9,7 +9,7 @@ import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/map
|
||||
import {TokenType} from "@github/actions-workflow-parser/templates/tokens/types";
|
||||
import {File} from "@github/actions-workflow-parser/workflows/file";
|
||||
import {Position, TextDocument} from "vscode-languageserver-textdocument";
|
||||
import {CompletionItem, TextEdit, Range} from "vscode-languageserver-types";
|
||||
import {CompletionItem, Range, TextEdit} from "vscode-languageserver-types";
|
||||
import {ContextProviderConfig} from "./context-providers/config";
|
||||
import {getContext, Mode} from "./context-providers/default";
|
||||
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
|
||||
@@ -77,10 +77,18 @@ export async function complete(
|
||||
token.definition?.definitionType === DefinitionType.String && (token.definition as StringDefinition).isExpression;
|
||||
const containsExpression = isString(token) && token.value.indexOf(OPEN_EXPRESSION) >= 0;
|
||||
if (isString(token) && (isExpression || containsExpression)) {
|
||||
const currentInput = token.value;
|
||||
const currentInput = token.source || token.value;
|
||||
|
||||
// Transform the overall position into a node relative position
|
||||
const relCharPos = newPos.character - token.range!.start[1];
|
||||
let relCharPos: number = 0;
|
||||
const lineDiff = newPos.line - token.range!.start[0];
|
||||
if (token.range!.start[0] !== token.range!.end[0]) {
|
||||
const lines = currentInput.split("\n");
|
||||
const linesBeforeCusor = lines.slice(0, lineDiff);
|
||||
relCharPos = linesBeforeCusor.join("\n").length + newPos.character;
|
||||
} else {
|
||||
relCharPos = newPos.character - token.range!.start[1];
|
||||
}
|
||||
|
||||
const expressionInput = (getExpressionInput(currentInput, relCharPos) || "").trim();
|
||||
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import {Position, TextDocument} from "vscode-languageserver-textdocument";
|
||||
import {createDocument} from "./document";
|
||||
|
||||
// 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] {
|
||||
/**
|
||||
* Calculates the position of the cursor and the document without that cursor
|
||||
* Cursor is represented by a `|` character
|
||||
* @param input Input string
|
||||
* @param skip Instances of `|` to skip
|
||||
*/
|
||||
export function getPositionFromCursor(input: string, skip = 0): [TextDocument, Position] {
|
||||
const doc = createDocument("test.yaml", input);
|
||||
|
||||
const cursorIndex = doc.getText().indexOf("|");
|
||||
let cursorIndex = doc.getText().indexOf("|");
|
||||
for (let i = 0; i < skip && cursorIndex !== -1; i++) {
|
||||
cursorIndex = doc.getText().indexOf("|", cursorIndex + 1);
|
||||
}
|
||||
|
||||
if (cursorIndex === -1) {
|
||||
throw new Error("No cursor found in document");
|
||||
}
|
||||
|
||||
// Replace only the last occurence of | in string
|
||||
let newText = doc.getText();
|
||||
newText = newText.substring(0, cursorIndex) + newText.substring(cursorIndex + 1);
|
||||
|
||||
const position = doc.positionAt(cursorIndex);
|
||||
const newDoc = TextDocument.create(doc.uri, doc.languageId, doc.version, doc.getText().replace("|", ""));
|
||||
const newDoc = TextDocument.create(doc.uri, doc.languageId, doc.version, newText);
|
||||
|
||||
return [newDoc, position];
|
||||
}
|
||||
|
||||
@@ -306,6 +306,128 @@ jobs:
|
||||
});
|
||||
});
|
||||
|
||||
describe("multi-line strings", () => {
|
||||
it("indented |", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
first line
|
||||
test \${{ github.does-not-exist }}
|
||||
test2`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Context access might be invalid: does-not-exist",
|
||||
range: {
|
||||
end: {
|
||||
character: 43,
|
||||
line: 7
|
||||
},
|
||||
start: {
|
||||
character: 15,
|
||||
line: 7
|
||||
}
|
||||
},
|
||||
severity: DiagnosticSeverity.Warning
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("indented |+", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |+
|
||||
first line
|
||||
test \${{ github.does-not-exist }}
|
||||
test2`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Context access might be invalid: does-not-exist",
|
||||
range: {
|
||||
end: {
|
||||
character: 43,
|
||||
line: 7
|
||||
},
|
||||
start: {
|
||||
character: 15,
|
||||
line: 7
|
||||
}
|
||||
},
|
||||
severity: DiagnosticSeverity.Warning
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("indented >", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: >
|
||||
first line
|
||||
test \${{ github.does-not-exist }}
|
||||
test2`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Context access might be invalid: does-not-exist",
|
||||
range: {
|
||||
end: {
|
||||
character: 43,
|
||||
line: 7
|
||||
},
|
||||
start: {
|
||||
character: 15,
|
||||
line: 7
|
||||
}
|
||||
},
|
||||
severity: DiagnosticSeverity.Warning
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("indented >+", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: >+
|
||||
first line
|
||||
test \${{ github.does-not-exist }}
|
||||
test2`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Context access might be invalid: does-not-exist",
|
||||
range: {
|
||||
end: {
|
||||
character: 43,
|
||||
line: 7
|
||||
},
|
||||
start: {
|
||||
character: 15,
|
||||
line: 7
|
||||
}
|
||||
},
|
||||
severity: DiagnosticSeverity.Warning
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("matrix context", () => {
|
||||
it("reference within a matrix job", async () => {
|
||||
const input = `
|
||||
|
||||
Generated
+6
-6
@@ -698,9 +698,9 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@github/actions-workflow-parser": {
|
||||
"version": "0.0.29",
|
||||
"resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.29/ecf3d1c9727e4f5f71e52863f456672b173b7096",
|
||||
"integrity": "sha512-VJLRv+GHD0E7S9uUCH89jIIA3iMdKRcqhrKwKq5TE5u/6Lap5nj1Juy48tBoCFAog07lhuqyqgWjP6FHHyAvPg==",
|
||||
"version": "0.0.31",
|
||||
"resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.31/ebc46956b91ed1a8c45efd41a3256ae818722557",
|
||||
"integrity": "sha512-3k+MBWG7Gn86aHeLdJTiYUTGm53npEelzleCbq/8Ir51xVkRINsyNe4HuGJizXxZ2WoTxAkLeZ6qpO1oCucSIg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-expressions": "*",
|
||||
@@ -13533,9 +13533,9 @@
|
||||
}
|
||||
},
|
||||
"@github/actions-workflow-parser": {
|
||||
"version": "0.0.29",
|
||||
"resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.29/ecf3d1c9727e4f5f71e52863f456672b173b7096",
|
||||
"integrity": "sha512-VJLRv+GHD0E7S9uUCH89jIIA3iMdKRcqhrKwKq5TE5u/6Lap5nj1Juy48tBoCFAog07lhuqyqgWjP6FHHyAvPg==",
|
||||
"version": "0.0.31",
|
||||
"resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.31/ebc46956b91ed1a8c45efd41a3256ae818722557",
|
||||
"integrity": "sha512-3k+MBWG7Gn86aHeLdJTiYUTGm53npEelzleCbq/8Ir51xVkRINsyNe4HuGJizXxZ2WoTxAkLeZ6qpO1oCucSIg==",
|
||||
"requires": {
|
||||
"@github/actions-expressions": "*",
|
||||
"yaml": "^2.0.0-8"
|
||||
|
||||
Reference in New Issue
Block a user