Files
languageservices/languageservice/src/utils/transform.ts
T
eric sciple d2ffb50a92 Add language service support for action.yml files (#275)
- Add validation, completion, hover, and document links for action.yml files
- Implement document type detection to route action.yml to action-specific handlers
- Add expression context for composite actions (inputs, steps, github, runner, etc.)
- Add schema validation for required fields, branding, and composite step requirements
- Support JavaScript (node20/node24), Docker, and composite action types
- Validate action references in composite action uses steps
- Add JSDoc comments to parser and template functions
- Refactor hover to use hoverToken consistently
- Fix lint errors and add return type annotations
2026-01-02 10:38:52 -06:00

103 lines
3.1 KiB
TypeScript

import {isString} from "@actions/workflow-parser";
import {StringToken} from "@actions/workflow-parser/templates/tokens/string-token";
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token";
import {Position, TextDocument} from "vscode-languageserver-textdocument";
import {Range} from "vscode-languageserver-types";
const PLACEHOLDER_KEY = "key";
/**
* Transforms a document to make it valid YAML so the parser can understand
* the cursor position during auto-completion.
*
* When typing in an IDE, the document is usually invalid YAML:
* - `runs-on` without `:` isn't a valid key
* - Empty lines don't parse as anything
* - `- ` without a value isn't complete
*
* This function inserts placeholders to make the document parseable:
* - Empty line → inserts `key:` placeholder
* - Line without colon → appends `:`
* - Sequence item `- ` → inserts `key` after the dash
*
* Lines containing `${{` are skipped to avoid breaking multi-line strings.
*
* The `isPlaceholder()` helper filters out the fake entries from completions.
*/
export function transform(doc: TextDocument, pos: Position): [TextDocument, Position] {
let offset = doc.offsetAt(pos);
const lineRange: Range = {
start: {line: pos.line, character: 0},
end: {line: pos.line, character: Number.MAX_SAFE_INTEGER}
};
let line = doc.getText(lineRange);
// If the line includes a new-line char, strip that out
const newLinePos = line.indexOf("\n");
if (newLinePos >= 0) {
line = line.substring(0, newLinePos);
}
lineRange.end.character = line.length;
const linePos = pos.character;
// Special case for Actions, if this line contains an expression marker, do _not_ transform. This is
// an ugly fix for auto-completion in multi-line YAML strings. At this point in the process, we cannot
// determine if a line is in such a multi-line string.
if (line.indexOf("${{") !== -1) {
return [doc, pos];
}
const containsColon = line.indexOf(":") !== -1;
if (!containsColon) {
const trimmedLine = line.trim();
if (trimmedLine === "" || trimmedLine === "-") {
// Pos in sequence or empty line
let spacer = "";
if (trimmedLine === "-" && !line.endsWith(" ")) {
spacer = " ";
offset++;
}
line =
line.substring(0, linePos) +
spacer +
PLACEHOLDER_KEY +
(trimmedLine === "-" ? "" : ":") +
line.substring(linePos);
// Adjust pos by one to prevent a sequence node being marked as active
offset++;
} else if (!trimmedLine.startsWith("-")) {
// Add `:` to end of line
line = line + ":";
}
}
const newDoc = TextDocument.create(doc.uri, doc.languageId, doc.version, doc.getText());
TextDocument.update(
newDoc,
[
{
range: lineRange,
text: line
}
],
newDoc.version + 1
);
return [newDoc, newDoc.positionAt(offset)];
}
// Detect placeholder key and value added by transform
export function isPlaceholder(key: StringToken, value: TemplateToken) {
if (key.value === PLACEHOLDER_KEY && isString(value) && value.value == "") {
return true;
}
return false;
}