diff --git a/languageserver/src/connection.ts b/languageserver/src/connection.ts index 274923a..09556e7 100644 --- a/languageserver/src/connection.ts +++ b/languageserver/src/connection.ts @@ -1,4 +1,4 @@ -import {documentLinks, hover, validate, ValidationConfig} from "@actions/languageservice"; +import {documentLinks, documentSymbols, hover, validate, ValidationConfig} from "@actions/languageservice"; import {registerLogger, setLogLevel} from "@actions/languageservice/log"; import {clearCache, clearCacheEntry} from "@actions/languageservice/utils/workflow-cache"; import {Octokit} from "@octokit/rest"; @@ -7,6 +7,8 @@ import { Connection, DocumentLink, DocumentLinkParams, + DocumentSymbol, + DocumentSymbolParams, ExecuteCommandParams, Hover, HoverParams, @@ -72,6 +74,10 @@ export function initConnection(connection: Connection) { hoverProvider: true, documentLinkProvider: { resolveProvider: false + }, + documentSymbolProvider: { + label: "GitHub Actions", + workDoneProgress: false } } }; @@ -158,6 +164,11 @@ export function initConnection(connection: Connection) { return documentLinks(getDocument(documents, textDocument), repoContext?.workspaceUri); }); + connection.onDocumentSymbol(async ({textDocument}: DocumentSymbolParams): Promise => { + const repoContext = repos.find(repo => textDocument.uri.startsWith(repo.workspaceUri)); + return documentSymbols(getDocument(documents, textDocument), repoContext?.workspaceUri); + }); + // Make the text document manager listen on the connection // for open, change and close text document events documents.listen(connection); diff --git a/languageservice/src/document-symbols.ts b/languageservice/src/document-symbols.ts new file mode 100644 index 0000000..166db84 --- /dev/null +++ b/languageservice/src/document-symbols.ts @@ -0,0 +1,70 @@ +import {ErrorPolicy} from "@actions/workflow-parser/model/convert"; +import {File} from "@actions/workflow-parser/workflows/file"; +import {TextDocument} from "vscode-languageserver-textdocument"; +import {DocumentSymbol, SymbolKind} from "vscode-languageserver-types"; +import {mapRange} from "./utils/range"; +import {fetchOrConvertWorkflowTemplate, fetchOrParseWorkflow} from "./utils/workflow-cache"; + +export async function documentSymbols( + document: TextDocument, + workspace: string | undefined +): Promise { + const file: File = { + name: document.uri, + content: document.getText() + }; + + const parsedWorkflow = fetchOrParseWorkflow(file, document.uri); + if (!parsedWorkflow?.value) { + return []; + } + + const template = await fetchOrConvertWorkflowTemplate( + parsedWorkflow.context, + parsedWorkflow.value, + document.uri, + undefined, + { + errorPolicy: ErrorPolicy.TryConversion + } + ); + + if (!template) { + return []; + } + + const symbols: DocumentSymbol[] = []; + + const onSymbol = DocumentSymbol.create( + "`on` Triggers", + undefined, + SymbolKind.Key, + mapRange(template.events.range), + mapRange(template.events.range) + ); + symbols.push(onSymbol); + + const jobsSymbol = DocumentSymbol.create( + "Jobs", + undefined, + SymbolKind.Namespace, + mapRange(template.jobs?.[0].id.range), + mapRange(template.jobs?.[0].id.range), + [] + ); + symbols.push(jobsSymbol); + + for (const job of template.jobs || []) { + const jobSymbol = DocumentSymbol.create( + `Job ${job.name?.toDisplayString() || job.id?.toString()}`, + "detail", + SymbolKind.Class, + mapRange(job.id.range), + mapRange(job.id.range) + ); + + jobsSymbol.children!.push(jobSymbol); + } + + return symbols; +} diff --git a/languageservice/src/index.ts b/languageservice/src/index.ts index c96eb20..c3d356f 100644 --- a/languageservice/src/index.ts +++ b/languageservice/src/index.ts @@ -1,7 +1,8 @@ export {complete} from "./complete"; export {ContextProviderConfig} from "./context-providers/config"; export {documentLinks} from "./document-links"; +export {documentSymbols} from "./document-symbols"; export {hover} from "./hover"; -export {Logger, LogLevel, registerLogger, setLogLevel} from "./log"; -export {validate, ValidationConfig, ActionsMetadataProvider} from "./validate"; +export {LogLevel, Logger, registerLogger, setLogLevel} from "./log"; +export {ActionsMetadataProvider, ValidationConfig, validate} from "./validate"; export {ValueProviderConfig, ValueProviderKind} from "./value-providers/config"; diff --git a/workflow-parser/src/model/convert.ts b/workflow-parser/src/model/convert.ts index 47e9373..eba4d54 100644 --- a/workflow-parser/src/model/convert.ts +++ b/workflow-parser/src/model/convert.ts @@ -9,7 +9,7 @@ import {handleTemplateTokenErrors} from "./converter/handle-errors"; import {convertJobs} from "./converter/jobs"; import {convertReferencedWorkflow} from "./converter/referencedWorkflow"; import {isReusableWorkflowJob} from "./type-guards"; -import {WorkflowTemplate} from "./workflow-template"; +import {EventsConfig, WorkflowTemplate} from "./workflow-template"; export enum ErrorPolicy { ReturnErrorsOnly, @@ -73,7 +73,9 @@ export async function convertWorkflowTemplate( switch (key.value) { case "on": - result.events = handleTemplateTokenErrors(root, context, {}, () => convertOn(context, item.value)); + result.events = handleTemplateTokenErrors(root, context, {range: item.key.range} as EventsConfig, () => + convertOn(context, item.value) + ); break; case "jobs": diff --git a/workflow-parser/src/model/converter/events.ts b/workflow-parser/src/model/converter/events.ts index dcfede6..04b767f 100644 --- a/workflow-parser/src/model/converter/events.ts +++ b/workflow-parser/src/model/converter/events.ts @@ -23,12 +23,15 @@ export function convertOn(context: TemplateContext, token: TemplateToken): Event const event = token.assertString("on"); return { + range: token.range, [event.value]: {} } as EventsConfig; } if (isSequence(token)) { - const result = {} as EventsConfig; + const result = { + range: token.range + } as EventsConfig; for (const item of token) { const event = item.assertString("on"); @@ -39,7 +42,9 @@ export function convertOn(context: TemplateContext, token: TemplateToken): Event } if (isMapping(token)) { - const result = {} as EventsConfig; + const result = { + range: token.range + } as EventsConfig; for (const item of token) { const eventKey = item.key.assertString("event name"); @@ -77,7 +82,9 @@ export function convertOn(context: TemplateContext, token: TemplateToken): Event } context.error(token, "Invalid format for 'on'"); - return {}; + return { + range: token.range + } as EventsConfig; } function convertPatternFilter( diff --git a/workflow-parser/src/model/workflow-template.ts b/workflow-parser/src/model/workflow-template.ts index ff3d172..ae4f5b8 100644 --- a/workflow-parser/src/model/workflow-template.ts +++ b/workflow-parser/src/model/workflow-template.ts @@ -6,6 +6,7 @@ import { StringToken, TemplateToken } from "../templates/tokens"; +import {TokenRange} from "../templates/tokens/token-range"; export type WorkflowTemplate = { events: EventsConfig; @@ -99,6 +100,8 @@ export type ActionStep = BaseStep & { }; export type EventsConfig = { + range: TokenRange; + schedule?: ScheduleConfig[]; workflow_dispatch?: WorkflowDispatchConfig; workflow_call?: WorkflowCallConfig;