Rework .reg importing in script editor

Script editor now supports drag and drop for .ps1, .reg, and other text files. .ps1 files will be handled by replacing the contents of the text editor. .reg files will be converted to PowerShell commands and inserted into the script editor. Any other text file will be inserted as an escaped string.
This commit is contained in:
Pat Hartl 2026-06-10 20:19:06 -05:00
parent 37caa8ab9b
commit ca713f6d3f
6 changed files with 251 additions and 8 deletions

View file

@ -80,8 +80,6 @@
<Button Icon="@IconType.Outline.FolderOpen" OnClick="BrowseForPath" Type="@ButtonType.Text">Browse</Button>
}
<Button Icon="@IconType.Outline.Build" OnClick="() => _regToPowerShell.Open()" Type="@ButtonType.Text">Import .reg</Button>
@if (IsDebuggable && HasDebugTarget)
{
<Tooltip Title="Debug">
@ -107,14 +105,12 @@
</FormItem>
</Form>
<RegToPowerShell @ref="_regToPowerShell" OnParsed="(text) => InsertText(text)" />
@code {
Guid _id = Guid.NewGuid();
Form<Script>? _form;
CodeInput? _codeInput;
RegToPowerShell? _regToPowerShell;
PowerShellConsole? _console;
IEnumerable<Snippet> Snippets { get; set; } = [];
List<string> _variables = new();

View file

@ -31,7 +31,6 @@
<Button Icon="@IconType.Outline.FolderOpen" OnClick="BrowseForPath" Type="@ButtonType.Text">Browse</Button>
}
<Button Icon="@IconType.Outline.Build" OnClick="() => RegToPowerShell.Open()" Type="@ButtonType.Text">Import .reg</Button>
</FormItem>
<FormItem>
@ -58,7 +57,6 @@
</FormItem>
</Form>
<RegToPowerShell @ref="RegToPowerShell" OnParsed="(text) => InsertText(text)" />
@code {
[Parameter] public Script Script { get; set; } = new();
@ -70,7 +68,6 @@
Guid Id = Guid.NewGuid();
Form<Script> Form;
MonacoCodeEditor? Editor;
RegToPowerShell RegToPowerShell;
IEnumerable<Snippet> Snippets { get; set; }
StandaloneEditorConstructionOptions EditorConstructionOptions(StandaloneCodeEditor editor)

View file

@ -96,6 +96,9 @@
await _module.InvokeVoidAsync("setScriptType", ScriptType);
}
if (Language.Id == "powershell")
await _module.InvokeVoidAsync("SetupFileDropHandler");
if (OnReady.HasDelegate)
await OnReady.InvokeAsync();
}

View file

@ -0,0 +1,244 @@
declare const monaco: any;
export function SetupFileDropHandler(): void {
if (typeof monaco === "undefined")
return;
const editors = monaco.editor.getEditors();
const editor = editors[editors.length - 1];
if (!editor)
return;
const domNode = editor.getDomNode();
if (!domNode)
return;
domNode.addEventListener("dragover", (e: DragEvent) => {
if (e.dataTransfer?.types?.includes("Files")) {
e.preventDefault();
e.stopPropagation();
}
});
domNode.addEventListener("drop", (e: DragEvent) => {
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
e.preventDefault();
e.stopPropagation();
handleFile(editor, files[0]);
}
});
document.addEventListener("paste", (e: ClipboardEvent) => {
if (!editor.hasWidgetFocus())
return;
if (!e.clipboardData?.files.length)
return;
e.preventDefault();
e.stopPropagation();
handleFile(editor, e.clipboardData.files[0]);
}, true);
}
const MAX_FILE_SIZE = 1024 * 1024; // 1 MB
function IsTextFile(file: File): boolean {
if (file.type.startsWith("text/")) return true;
const textExtensions = [
".reg", ".ps1", ".txt", ".cfg", ".ini", ".conf",
".json", ".xml", ".yaml", ".yml", ".csv", ".log",
".bat", ".cmd", ".sh", ".py", ".lua",
];
return textExtensions.includes(getFileExtension(file.name));
}
function handleFile(editor: any, file: File): void {
if (file.size > MAX_FILE_SIZE)
return;
if (!IsTextFile(file))
return;
const reader = new FileReader();
reader.onload = () => {
const content = reader.result as string;
const ext = getFileExtension(file.name);
if (ext === ".reg") {
const psCommands = ParseRegToPowerShell(content);
insertTextAtCursor(editor, psCommands);
} else if (ext === ".ps1") {
editor.setValue(content);
} else {
const hereString = `@'\n${content}\n'@`;
insertTextAtCursor(editor, hereString);
}
};
reader.readAsText(file);
}
function getFileExtension(filename: string): string {
const idx = filename.lastIndexOf(".");
return idx >= 0 ? filename.substring(idx).toLowerCase() : "";
}
function insertTextAtCursor(editor: any, text: string): void {
const selection = editor.getSelection();
editor.executeEdits("fileImport", [{
range: selection,
text: text,
forceMoveMarkers: true,
}]);
}
interface RegEntry {
type: "key" | "sz" | "dword" | "binary";
path: string;
property?: string;
value?: any;
}
function ParseRegToPowerShell(regContent: string): string {
const entries = ParseRegFile(regContent);
const lines: string[] = [];
for (const entry of entries) {
switch (entry.type) {
case "key":
if (lines.length > 0)
lines.push("");
lines.push(`New-Item -Path "registry::\\${entry.path}"`);
break;
case "sz": {
const escaped = (entry.value as string).replace(/"/g, '""""');
lines.push(`New-ItemProperty -Path "registry::\\${entry.path}" -Name "${entry.property}" -Value "${escaped}" -Force`);
break;
}
case "dword":
lines.push(`New-ItemProperty -Path "registry::\\${entry.path}" -Name "${entry.property}" -Value ${entry.value} -Force`);
break;
case "binary": {
const bytes = entry.value as number[];
const chunks: string[] = [];
for (let i = 0; i < bytes.length; i += 32) {
const chunk = bytes.slice(i, i + 32);
chunks.push(chunk.map(b => "0x" + b.toString(16).toUpperCase().padStart(2, "0")).join(", "));
}
const formatted = chunks.join("\n\t");
lines.push(`New-ItemProperty -Path "registry::\\${entry.path}" -Name "${entry.property}" -PropertyType Binary -Value ([byte[]]@(\n\t${formatted}\n)) -Force`);
break;
}
}
}
return lines.join("\n");
}
function ParseRegFile(content: string): RegEntry[] {
const entries: RegEntry[] = [];
const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
const joined = normalized.replace(/\\\n\s*/g, "");
const rawLines = joined.split("\n");
let currentPath = "";
for (const line of rawLines) {
const trimmed = line.trim();
// Skip empty lines, comments, and headers
if (!trimmed || trimmed.startsWith(";") ||
trimmed === "Windows Registry Editor Version 5.00" ||
trimmed === "REGEDIT4") {
continue;
}
// Key section: [HKEY_...]
const keyMatch = trimmed.match(/^\[(.+)\]$/);
if (keyMatch) {
const path = keyMatch[1];
if (path.startsWith("-"))
continue;
currentPath = path;
entries.push({ type: "key", path: currentPath });
continue;
}
if (!currentPath)
continue;
// Parse value lines
let name: string;
let valueStr: string;
if (trimmed.startsWith("@=")) {
name = "@";
valueStr = trimmed.substring(2);
} else {
const valueMatch = trimmed.match(/^"(.+?)"=(.*)$/);
if (!valueMatch)
continue;
name = valueMatch[1].replace(/\\\\/g, "\\").replace(/\\"/g, '"');
valueStr = valueMatch[2];
}
// Skip delete values
if (valueStr === "-")
continue;
// String value: "..."
if (valueStr.startsWith('"') && valueStr.endsWith('"')) {
const str = valueStr.slice(1, -1).replace(/\\\\/g, "\\").replace(/\\"/g, '"');
entries.push({ type: "sz", path: currentPath, property: name, value: str });
continue;
}
// DWORD: dword:XXXXXXXX
const dwordMatch = valueStr.match(/^dword:([0-9a-fA-F]{1,8})$/);
if (dwordMatch) {
const val = parseInt(dwordMatch[1], 16);
entries.push({ type: "dword", path: currentPath, property: name, value: val });
continue;
}
// Binary/hex types: hex:XX,XX,... or hex(N):XX,XX,...
const hexMatch = valueStr.match(/^hex(?:\([0-9a-fA-F]+\))?:(.*)$/);
if (hexMatch) {
const hexStr = hexMatch[1].replace(/\s/g, "");
const bytes = hexStr.split(",")
.filter(s => s.length > 0)
.map(s => parseInt(s, 16));
entries.push({ type: "binary", path: currentPath, property: name, value: bytes });
}
}
return entries;
}

View file

@ -67,6 +67,8 @@ public class MonacoCodeEditor : StandaloneCodeEditor
_previousScriptType = ScriptType;
await _module.InvokeVoidAsync("setScriptType", ScriptType);
}
await _module.InvokeVoidAsync("SetupFileDropHandler");
}
private async Task OnChanged(ModelContentChangedEvent e)

View file

@ -8,4 +8,5 @@ export { Terminal } from "./Components/Terminal/Terminal";
export { DomHelper } from "./Components/DomHelper/DomHelper";
export { registerPowerShellCompletions, setScriptType, validateScript, insertSnippet, getScriptTemplate } from "./Components/MonacoCodeEditor/PowerShellCompletionProvider";
export { registerYamlCompletions } from "./Components/MonacoCodeEditor/YamlCompletionProvider";
export { UploadManager } from "./Components/UploadManager/UploadManager";
export { UploadManager } from "./Components/UploadManager/UploadManager";
export { SetupFileDropHandler } from "./Components/CodeInput/FileDropHandler";