Fix scaffolding snippets to replace typed text instead of inserting (#307)

This commit is contained in:
eric sciple
2026-01-21 15:41:25 -06:00
committed by GitHub
parent 22aa458809
commit 74d69b24ab
3 changed files with 59 additions and 19 deletions
@@ -553,5 +553,31 @@ runs:
expect(labels).not.toContain("Composite Action");
expect(labels).not.toContain("Docker Action");
});
it("replaces typed text when selecting scaffolding snippet", async () => {
// User typed "compo" and then triggered completion
const [doc, position] = createActionDocument(`compo|`);
const completions = await complete(doc, position, scaffoldingConfig);
const compositeSnippet = completions.find(c => c.label === "Composite Action");
expect(compositeSnippet).toBeDefined();
// The textEdit should replace "compo", not insert after it
const textEdit = compositeSnippet?.textEdit as {range: {start: {character: number}; end: {character: number}}};
expect(textEdit.range.start.character).toBe(0); // Start of "compo"
expect(textEdit.range.end.character).toBe(5); // End of "compo"
});
it("handles empty file with no typed text", async () => {
const [doc, position] = createActionDocument(`|`);
const completions = await complete(doc, position, scaffoldingConfig);
const compositeSnippet = completions.find(c => c.label === "Composite Action");
const textEdit = compositeSnippet?.textEdit as {range: {start: {character: number}; end: {character: number}}};
// Zero-length range is fine when there's nothing to replace
expect(textEdit.range.start.character).toBe(0);
expect(textEdit.range.end.character).toBe(0);
});
});
});
+27 -13
View File
@@ -1,7 +1,7 @@
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/index";
import {MappingToken} from "@actions/workflow-parser/templates/tokens/mapping-token";
import {Position} from "vscode-languageserver-textdocument";
import {CompletionItem, CompletionItemKind, InsertTextFormat, TextEdit} from "vscode-languageserver-types";
import {CompletionItem, CompletionItemKind, InsertTextFormat, Range, TextEdit} from "vscode-languageserver-types";
import {Value} from "./value-providers/config.js";
/**
@@ -320,7 +320,8 @@ export function filterActionRunsCompletions(values: Value[], path: TemplateToken
export function getActionScaffoldingSnippets(
root: TemplateToken | undefined,
path: TemplateToken[],
position: Position
position: Position,
replaceRange?: Range
): CompletionItem[] {
// Get the runs mapping from the root, if it exists
let runsMapping: MappingToken | undefined;
@@ -354,21 +355,24 @@ export function getActionScaffoldingSnippets(
"Scaffold a Node.js action\n\n[Documentation](https://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-javascript-action)",
ACTION_SNIPPET_NODEJS_USING,
position,
"0_nodejs"
"0_nodejs",
replaceRange
),
createSnippetCompletion(
"Composite Action",
"Scaffold a composite action\n\n[Documentation](https://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-composite-action)",
ACTION_SNIPPET_COMPOSITE_USING,
position,
"1_composite"
"1_composite",
replaceRange
),
createSnippetCompletion(
"Docker Action",
"Scaffold a Docker action\n\n[Documentation](https://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-docker-container-action)",
ACTION_SNIPPET_DOCKER_USING,
position,
"2_docker"
"2_docker",
replaceRange
)
];
}
@@ -399,21 +403,24 @@ export function getActionScaffoldingSnippets(
"Scaffold a Node.js action\n\n[Documentation](https://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-javascript-action)",
ACTION_SNIPPET_NODEJS_RUNS,
position,
"1_nodejs"
"1_nodejs",
replaceRange
),
createSnippetCompletion(
"Composite Action",
"Scaffold a composite action\n\n[Documentation](https://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-composite-action)",
ACTION_SNIPPET_COMPOSITE_RUNS,
position,
"2_composite"
"2_composite",
replaceRange
),
createSnippetCompletion(
"Docker Action",
"Scaffold a Docker action\n\n[Documentation](https://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-docker-container-action)",
ACTION_SNIPPET_DOCKER_RUNS,
position,
"3_docker"
"3_docker",
replaceRange
)
];
}
@@ -425,21 +432,24 @@ export function getActionScaffoldingSnippets(
"Scaffold a complete Node.js action\n\n[Documentation](https://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-javascript-action)",
ACTION_SNIPPET_NODEJS_FULL,
position,
"1_nodejs"
"1_nodejs",
replaceRange
),
createSnippetCompletion(
"Composite Action",
"Scaffold a complete composite action\n\n[Documentation](https://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-composite-action)",
ACTION_SNIPPET_COMPOSITE_FULL,
position,
"2_composite"
"2_composite",
replaceRange
),
createSnippetCompletion(
"Docker Action",
"Scaffold a complete Docker action\n\n[Documentation](https://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-docker-container-action)",
ACTION_SNIPPET_DOCKER_FULL,
position,
"3_docker"
"3_docker",
replaceRange
)
];
}
@@ -452,8 +462,12 @@ function createSnippetCompletion(
description: string,
snippetText: string,
position: Position,
sortText: string
sortText: string,
replaceRange?: Range
): CompletionItem {
// Use replace if we have a range, otherwise insert at position
const textEdit = replaceRange ? TextEdit.replace(replaceRange, snippetText) : TextEdit.insert(position, snippetText);
return {
label,
kind: CompletionItemKind.Snippet,
@@ -463,6 +477,6 @@ function createSnippetCompletion(
},
insertTextFormat: InsertTextFormat.Snippet,
sortText,
textEdit: TextEdit.insert(position, snippetText)
textEdit
};
}
+6 -6
View File
@@ -158,12 +158,6 @@ export async function complete(
const escapeHatches = getEscapeHatchCompletions(token, keyToken, indentString, newPos, schema);
values.push(...escapeHatches);
// Get action scaffolding snippets if applicable
let actionSnippets: CompletionItem[] = [];
if (isAction && config?.featureFlags?.isEnabled("actionScaffoldingSnippets")) {
actionSnippets = getActionScaffoldingSnippets(parsedTemplate.value, path, position);
}
// Figure out what text to replace when the user picks a completion.
// For example, if they typed `runs-|` and pick `runs-on`, we need to replace `runs-`.
let replaceRange: Range | undefined;
@@ -191,6 +185,12 @@ export async function complete(
}
}
// Get action scaffolding snippets if applicable
let actionSnippets: CompletionItem[] = [];
if (isAction && config?.featureFlags?.isEnabled("actionScaffoldingSnippets")) {
actionSnippets = getActionScaffoldingSnippets(parsedTemplate.value, path, position, replaceRange);
}
// Convert values to LSP CompletionItems
const completionItems = values.map(value => {
const newText = value.insertText || value.label;