Replace cron info diagnostics with inlay hints (#270)
- Remove DiagnosticSeverity.Information for valid cron expressions - Add new inlay-hints.ts module with getInlayHints() function - Register inlayHintProvider capability in language server - Display human-readable cron descriptions as inline hints Related #269
This commit is contained in:
@@ -2,6 +2,7 @@ export {complete} from "./complete.js";
|
||||
export {ContextProviderConfig} from "./context-providers/config.js";
|
||||
export {documentLinks} from "./document-links.js";
|
||||
export {hover} from "./hover.js";
|
||||
export {getInlayHints} from "./inlay-hints.js";
|
||||
export {Logger, LogLevel, registerLogger, setLogLevel} from "./log.js";
|
||||
export {validate, ValidationConfig, ActionsMetadataProvider} from "./validate.js";
|
||||
export {ValueProviderConfig, ValueProviderKind} from "./value-providers/config.js";
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import {InlayHintKind} from "vscode-languageserver-types";
|
||||
import {getInlayHints} from "./inlay-hints.js";
|
||||
import {registerLogger} from "./log.js";
|
||||
import {createDocument} from "./test-utils/document.js";
|
||||
import {TestLogger} from "./test-utils/logger.js";
|
||||
import {clearCache} from "./utils/workflow-cache.js";
|
||||
|
||||
registerLogger(new TestLogger());
|
||||
|
||||
beforeEach(() => {
|
||||
clearCache();
|
||||
});
|
||||
|
||||
describe("inlay-hints", () => {
|
||||
describe("cron expressions", () => {
|
||||
it("returns inlay hint for valid cron expression", () => {
|
||||
const input = `on:
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
`;
|
||||
const document = createDocument("test.yaml", input);
|
||||
const hints = getInlayHints(document);
|
||||
|
||||
expect(hints).toHaveLength(1);
|
||||
expect(hints[0].label).toBe("→ Runs every hour");
|
||||
expect(hints[0].kind).toBe(InlayHintKind.Parameter);
|
||||
expect(hints[0].paddingLeft).toBe(true);
|
||||
});
|
||||
|
||||
it("returns correct position at end of cron value", () => {
|
||||
const input = `on:
|
||||
schedule:
|
||||
- cron: '0 3 * * 1'
|
||||
`;
|
||||
const document = createDocument("test.yaml", input);
|
||||
const hints = getInlayHints(document);
|
||||
|
||||
expect(hints).toHaveLength(1);
|
||||
// Position should be at the end of the cron string value (after the closing quote)
|
||||
// Line 3 (0-indexed: 2), end of '0 3 * * 1'
|
||||
expect(hints[0].position.line).toBe(2);
|
||||
});
|
||||
|
||||
it("returns no hint for invalid cron expression", () => {
|
||||
const input = `on:
|
||||
schedule:
|
||||
- cron: 'invalid cron'
|
||||
`;
|
||||
const document = createDocument("test.yaml", input);
|
||||
const hints = getInlayHints(document);
|
||||
|
||||
expect(hints).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("returns multiple hints for multiple cron expressions", () => {
|
||||
const input = `on:
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
- cron: '0 0 * * *'
|
||||
`;
|
||||
const document = createDocument("test.yaml", input);
|
||||
const hints = getInlayHints(document);
|
||||
|
||||
expect(hints).toHaveLength(2);
|
||||
expect(hints[0].label).toBe("→ Runs every hour");
|
||||
expect(hints[1].label).toBe("→ Runs at 00:00");
|
||||
});
|
||||
|
||||
it("returns hint with descriptive label for weekly cron", () => {
|
||||
const input = `on:
|
||||
schedule:
|
||||
- cron: '0 3 * * 1'
|
||||
`;
|
||||
const document = createDocument("test.yaml", input);
|
||||
const hints = getInlayHints(document);
|
||||
|
||||
expect(hints).toHaveLength(1);
|
||||
expect(hints[0].label).toContain("Monday");
|
||||
});
|
||||
|
||||
it("returns no hints for empty workflow", () => {
|
||||
const input = ``;
|
||||
const document = createDocument("test.yaml", input);
|
||||
const hints = getInlayHints(document);
|
||||
|
||||
expect(hints).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("returns no hints for workflow without schedule", () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hello
|
||||
`;
|
||||
const document = createDocument("test.yaml", input);
|
||||
const hints = getInlayHints(document);
|
||||
|
||||
expect(hints).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("returns hint for frequent cron that triggers warning", () => {
|
||||
// Even crons that trigger the <5min warning should still get inlay hints
|
||||
const input = `on:
|
||||
schedule:
|
||||
- cron: '* * * * *'
|
||||
`;
|
||||
const document = createDocument("test.yaml", input);
|
||||
const hints = getInlayHints(document);
|
||||
|
||||
expect(hints).toHaveLength(1);
|
||||
expect(hints[0].label).toBe("→ Runs every minute");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import {isString} from "@actions/workflow-parser";
|
||||
import {getCronDescription} from "@actions/workflow-parser/model/converter/cron";
|
||||
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token";
|
||||
import {File} from "@actions/workflow-parser/workflows/file";
|
||||
import {TextDocument} from "vscode-languageserver-textdocument";
|
||||
import {InlayHint, InlayHintKind} from "vscode-languageserver-types";
|
||||
import {fetchOrParseWorkflow} from "./utils/workflow-cache.js";
|
||||
|
||||
/**
|
||||
* Returns inlay hints for a workflow document.
|
||||
* Currently supports cron expressions, showing a human-readable description
|
||||
* of the schedule inline after the cron value.
|
||||
*
|
||||
* @param document Text document to get inlay hints for
|
||||
* @returns Array of inlay hints
|
||||
*/
|
||||
export function getInlayHints(document: TextDocument): InlayHint[] {
|
||||
const file: File = {
|
||||
name: document.uri,
|
||||
content: document.getText()
|
||||
};
|
||||
|
||||
const result = fetchOrParseWorkflow(file, document.uri);
|
||||
if (!result?.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const hints: InlayHint[] = [];
|
||||
|
||||
// Traverse the workflow AST to find cron expressions
|
||||
for (const [parent, token, key] of TemplateToken.traverse(result.value)) {
|
||||
const validationToken = key || parent || token;
|
||||
const validationDefinition = validationToken.definition;
|
||||
|
||||
// Check for cron-pattern tokens
|
||||
if (isString(token) && token.range && validationDefinition?.key === "cron-pattern") {
|
||||
const cronValue = token.value;
|
||||
const description = getCronDescription(cronValue);
|
||||
|
||||
if (description) {
|
||||
// Position the hint at the end of the cron value
|
||||
hints.push({
|
||||
position: {
|
||||
line: token.range.end.line - 1, // Convert from 1-based to 0-based
|
||||
character: token.range.end.column - 1 // Convert from 1-based to 0-based
|
||||
},
|
||||
label: `→ ${description}`,
|
||||
kind: InlayHintKind.Parameter,
|
||||
paddingLeft: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hints;
|
||||
}
|
||||
@@ -231,7 +231,7 @@ jobs:
|
||||
} as Diagnostic);
|
||||
});
|
||||
|
||||
it("cron with interval of 5 minutes or more shows info", async () => {
|
||||
it("cron with interval of 5 minutes or more shows no diagnostic", async () => {
|
||||
const result = await validate(
|
||||
createDocument(
|
||||
"wf.yaml",
|
||||
@@ -245,25 +245,7 @@ jobs:
|
||||
{valueProviderConfig: defaultValueProviders}
|
||||
);
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0]).toEqual({
|
||||
message: "Runs every 5 minutes",
|
||||
severity: DiagnosticSeverity.Information,
|
||||
code: "on-schedule",
|
||||
codeDescription: {
|
||||
href: "https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#onschedule"
|
||||
},
|
||||
range: {
|
||||
end: {
|
||||
character: 25,
|
||||
line: 2
|
||||
},
|
||||
start: {
|
||||
character: 12,
|
||||
line: 2
|
||||
}
|
||||
}
|
||||
} as Diagnostic);
|
||||
expect(result.length).toBe(0);
|
||||
});
|
||||
|
||||
it("cron with comma-separated minutes less than 5 apart shows warning", async () => {
|
||||
|
||||
@@ -258,17 +258,6 @@ function validateCronExpression(diagnostics: Diagnostic[], token: StringToken):
|
||||
href: CRON_SCHEDULE_DOCS_URL
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Show info message for valid cron expressions
|
||||
diagnostics.push({
|
||||
message: description,
|
||||
range: mapRange(token.range),
|
||||
severity: DiagnosticSeverity.Information,
|
||||
code: "on-schedule",
|
||||
codeDescription: {
|
||||
href: CRON_SCHEDULE_DOCS_URL
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user