From efe71dd684866c21156bfa84235b0a4d67cb22c8 Mon Sep 17 00:00:00 2001 From: Christopher Schleiden Date: Thu, 8 Dec 2022 16:38:36 -0800 Subject: [PATCH] Auto completion for multi-line expressions --- .../src/complete.expressions.test.ts | 58 +++++++++++++++++++ actions-languageservice/src/complete.ts | 14 ++++- .../src/test-utils/cursor-position.ts | 22 +++++-- 3 files changed, 86 insertions(+), 8 deletions(-) diff --git a/actions-languageservice/src/complete.expressions.test.ts b/actions-languageservice/src/complete.expressions.test.ts index 7a2d345..a32c9dd 100644 --- a/actions-languageservice/src/complete.expressions.test.ts +++ b/actions-languageservice/src/complete.expressions.test.ts @@ -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); diff --git a/actions-languageservice/src/complete.ts b/actions-languageservice/src/complete.ts index 067680c..5ecf738 100644 --- a/actions-languageservice/src/complete.ts +++ b/actions-languageservice/src/complete.ts @@ -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(); diff --git a/actions-languageservice/src/test-utils/cursor-position.ts b/actions-languageservice/src/test-utils/cursor-position.ts index 51d2ed4..c26c0e1 100644 --- a/actions-languageservice/src/test-utils/cursor-position.ts +++ b/actions-languageservice/src/test-utils/cursor-position.ts @@ -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]; }