diff --git a/actions-languageserver/src/connection.ts b/actions-languageserver/src/connection.ts index c610947..ef3114d 100644 --- a/actions-languageserver/src/connection.ts +++ b/actions-languageserver/src/connection.ts @@ -1,10 +1,11 @@ -import {hover} from "@github/actions-languageservice/hover"; +import {documentLinks, hover, validate, ValidationConfig} from "@github/actions-languageservice"; import {registerLogger, setLogLevel} from "@github/actions-languageservice/log"; -import {validate, ValidationConfig} from "@github/actions-languageservice/validate"; import {Octokit} from "@octokit/rest"; import { CompletionItem, Connection, + DocumentLink, + DocumentLinkParams, Hover, HoverParams, InitializeParams, @@ -61,7 +62,10 @@ export function initConnection(connection: Connection) { resolveProvider: false, triggerCharacters: [":", "."] }, - hoverProvider: true + hoverProvider: true, + documentLinkProvider: { + resolveProvider: false + } } }; @@ -103,7 +107,6 @@ export function initConnection(connection: Connection) { connection.sendDiagnostics({uri: textDocument.uri, diagnostics: result}); } - // This handler provides the initial list of the completion items. connection.onCompletion(async ({position, textDocument}: TextDocumentPositionParams): Promise => { return await onCompletion( position, @@ -118,6 +121,10 @@ export function initConnection(connection: Connection) { return hover(documents.get(textDocument.uri)!, position); }); + connection.onDocumentLinks(async ({textDocument}: DocumentLinkParams): Promise => { + return documentLinks(documents.get(textDocument.uri)!); + }); + // Make the text document manager listen on the connection // for open, change and close text document events documents.listen(connection); diff --git a/actions-languageserver/src/context-providers.ts b/actions-languageserver/src/context-providers.ts index edc8cfa..8d37724 100644 --- a/actions-languageserver/src/context-providers.ts +++ b/actions-languageserver/src/context-providers.ts @@ -1,5 +1,5 @@ import {data} from "@github/actions-expressions"; -import {ContextProviderConfig} from "@github/actions-languageservice/."; +import {ContextProviderConfig} from "@github/actions-languageservice"; import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context"; import {Octokit} from "@octokit/rest"; import {getSecrets} from "./context-providers/secrets"; diff --git a/actions-languageservice/src/document-links.test.ts b/actions-languageservice/src/document-links.test.ts new file mode 100644 index 0000000..576c0de --- /dev/null +++ b/actions-languageservice/src/document-links.test.ts @@ -0,0 +1,80 @@ +import {documentLinks} from "./document-links"; +import {createDocument} from "./test-utils/document"; + +describe("documentLinks", () => { + it("no links without actions", async () => { + const input = `on: push +jobs: + build: + runs-on: [self-hosted] + steps: + - run: echo "Hello World"`; + const result = await documentLinks(createDocument("test.yaml", input)); + expect(result).toHaveLength(0); + }); + + it("no links for invalid workflow", async () => { + const input = `onFOO: push +jobs: + build: + runs-on: [self-hosted]`; + const result = await documentLinks(createDocument("test.yaml", input)); + expect(result).toHaveLength(0); + }); + + it("links for actions in workflow", async () => { + const input = `on: push +jobs: + build: + runs-on: [self-hosted] + steps: + - uses: actions/checkout@v2 + - uses: actions/checkout@v3 + - uses: github/codeql-action/init@v2`; + const result = await documentLinks(createDocument("test.yaml", input)); + expect(result).toEqual([ + { + range: { + end: { + character: 31, + line: 5 + }, + start: { + character: 12, + line: 5 + } + }, + target: "https://www.github.com/actions/checkout/tree/v2/", + tooltip: "Open action on GitHub" + }, + { + range: { + end: { + character: 31, + line: 6 + }, + start: { + character: 12, + line: 6 + } + }, + target: "https://www.github.com/actions/checkout/tree/v3/", + tooltip: "Open action on GitHub" + }, + { + range: { + end: { + character: 40, + line: 7 + }, + start: { + character: 12, + line: 7 + } + }, + target: "https://www.github.com/github/codeql-action/tree/v2/init", + tooltip: "Open action on GitHub" + } + ]); + }); +}); diff --git a/actions-languageservice/src/document-links.ts b/actions-languageservice/src/document-links.ts new file mode 100644 index 0000000..8bc17cb --- /dev/null +++ b/actions-languageservice/src/document-links.ts @@ -0,0 +1,51 @@ +import {convertWorkflowTemplate, parseWorkflow} from "@github/actions-workflow-parser"; +import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert"; +import {File} from "@github/actions-workflow-parser/workflows/file"; +import {TextDocument} from "vscode-languageserver-textdocument"; +import {DocumentLink} from "vscode-languageserver-types"; +import {parseActionReference} from "./action"; +import {nullTrace} from "./nulltrace"; +import {mapRange} from "./utils/range"; + +export async function documentLinks(document: TextDocument): Promise { + const file: File = { + name: document.uri, + content: document.getText() + }; + + const result = parseWorkflow(file.name, [file], nullTrace); + if (!result.value) { + return []; + } + + const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion); + + // Add links to referenced actions + const actionLinks: DocumentLink[] = []; + + // TODO: Support base uri for GHES + const gitHubBaseUri = "https://www.github.com/"; + + for (const job of template?.jobs || []) { + for (const step of job?.steps || []) { + if ("uses" in step) { + const actionRef = parseActionReference(step.uses.value); + if (!actionRef) { + continue; + } + + const url = `${gitHubBaseUri}${actionRef.owner}/${actionRef.name}/tree/${actionRef.ref}/${ + actionRef.path || "" + }`; + + actionLinks.push({ + range: mapRange(step.uses.range), + target: url, + tooltip: `Open action on GitHub` + }); + } + } + } + + return [...actionLinks]; +} diff --git a/actions-languageservice/src/index.ts b/actions-languageservice/src/index.ts index fe24146..4e43252 100644 --- a/actions-languageservice/src/index.ts +++ b/actions-languageservice/src/index.ts @@ -1,6 +1,7 @@ export {complete} from "./complete"; export {ContextProviderConfig} from "./context-providers/config"; +export {documentLinks} from "./document-links"; export {hover} from "./hover"; export {Logger, LogLevel, registerLogger, setLogLevel} from "./log"; -export {validate} from "./validate"; +export {validate, ValidationConfig} from "./validate"; export {ValueProviderConfig, ValueProviderKind} from "./value-providers/config"; diff --git a/actions-languageservice/src/utils/find-token.test.ts b/actions-languageservice/src/utils/find-token.test.ts index c694311..d1e5f75 100644 --- a/actions-languageservice/src/utils/find-token.test.ts +++ b/actions-languageservice/src/utils/find-token.test.ts @@ -1,4 +1,4 @@ -import {isScalar, parseWorkflow} from "@github/actions-workflow-parser/."; +import {isScalar, parseWorkflow} from "@github/actions-workflow-parser"; import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token"; import {TokenType} from "@github/actions-workflow-parser/templates/tokens/types"; import {nullTrace} from "../nulltrace";