NewExtensionForm.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System.IO.Compression; 6 using System.Text.Json; 7 using System.Text.Json.Nodes; 8 using Microsoft.CommandPalette.Extensions; 9 using Microsoft.CommandPalette.Extensions.Toolkit; 10 11 namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands; 12 13 internal sealed partial class NewExtensionForm : NewExtensionFormBase 14 { 15 private static readonly string _creatingText = "Creating new extension..."; 16 private readonly StatusMessage _creatingMessage = new() 17 { 18 Message = _creatingText, 19 Progress = new ProgressState() { IsIndeterminate = true }, 20 }; 21 22 public NewExtensionForm() 23 { 24 TemplateJson = $$""" 25 { 26 "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", 27 "type": "AdaptiveCard", 28 "version": "1.6", 29 "body": [ 30 { 31 "type": "TextBlock", 32 "text": {{FormatJsonString(Properties.Resources.builtin_create_extension_page_title)}}, 33 "size": "large" 34 }, 35 { 36 "type": "Input.Text", 37 "label": {{FormatJsonString(Properties.Resources.builtin_create_extension_name_label)}}, 38 "isRequired": true, 39 "errorMessage": {{FormatJsonString(Properties.Resources.builtin_create_extension_name_required)}}, 40 "id": "ExtensionName", 41 "placeholder": "ExtensionName", 42 "regex": "^[a-zA-Z_][a-zA-Z0-9_]*$" 43 }, 44 { 45 "type": "TextBlock", 46 "text": {{FormatJsonString(Properties.Resources.builtin_create_extension_name_description)}}, 47 "wrap": true, 48 "size": "small", 49 "isSubtle": true, 50 "spacing": "none" 51 }, 52 { 53 "type": "Input.Text", 54 "label": {{FormatJsonString(Properties.Resources.builtin_create_extension_display_name_label)}}, 55 "isRequired": true, 56 "errorMessage": {{FormatJsonString(Properties.Resources.builtin_create_extension_display_name_required)}}, 57 "id": "DisplayName", 58 "placeholder": "My new extension", 59 "spacing": "medium" 60 }, 61 { 62 "type": "TextBlock", 63 "text": {{FormatJsonString(Properties.Resources.builtin_create_extension_display_name_description)}}, 64 "wrap": true, 65 "size": "small", 66 "isSubtle": true, 67 "spacing": "none" 68 }, 69 { 70 "type": "Input.Text", 71 "label": {{FormatJsonString(Properties.Resources.builtin_create_extension_directory_label)}}, 72 "isRequired": true, 73 "errorMessage": {{FormatJsonString(Properties.Resources.builtin_create_extension_directory_required)}}, 74 "id": "OutputPath", 75 "placeholder": "C:\\users\\me\\dev", 76 "spacing": "medium" 77 }, 78 { 79 "type": "TextBlock", 80 "text": {{FormatJsonString(Properties.Resources.builtin_create_extension_directory_description)}}, 81 "wrap": true, 82 "size": "small", 83 "isSubtle": true, 84 "spacing": "none" 85 } 86 ], 87 "actions": [ 88 { 89 "type": "Action.Submit", 90 "title": {{FormatJsonString(Properties.Resources.builtin_create_extension_submit)}}, 91 "associatedInputs": "auto" 92 } 93 ] 94 } 95 """; 96 } 97 98 public override CommandResult SubmitForm(string payload) 99 { 100 var formInput = JsonNode.Parse(payload)?.AsObject(); 101 if (formInput is null) 102 { 103 return CommandResult.KeepOpen(); 104 } 105 106 var extensionName = formInput["ExtensionName"]?.AsValue()?.ToString() ?? string.Empty; 107 var displayName = formInput["DisplayName"]?.AsValue()?.ToString() ?? string.Empty; 108 var outputPath = formInput["OutputPath"]?.AsValue()?.ToString() ?? string.Empty; 109 110 _creatingMessage.State = MessageState.Info; 111 _creatingMessage.Message = _creatingText; 112 _creatingMessage.Progress = new ProgressState() { IsIndeterminate = true }; 113 BuiltinsExtensionHost.Instance.ShowStatus(_creatingMessage, StatusContext.Extension); 114 115 try 116 { 117 CreateExtension(extensionName, displayName, outputPath); 118 119 BuiltinsExtensionHost.Instance.HideStatus(_creatingMessage); 120 121 RaiseFormSubmit(new CreatedExtensionForm(extensionName, displayName, outputPath)); 122 } 123 catch (Exception e) 124 { 125 BuiltinsExtensionHost.Instance.HideStatus(_creatingMessage); 126 127 _creatingMessage.State = MessageState.Error; 128 _creatingMessage.Message = $"Error: {e.Message}"; 129 } 130 131 return CommandResult.KeepOpen(); 132 } 133 134 private void CreateExtension(string extensionName, string newDisplayName, string outputPath) 135 { 136 var newGuid = Guid.NewGuid().ToString(); 137 138 // Unzip `template.zip` to a temp dir: 139 var tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); 140 141 // Does the output path exist? 142 if (!Directory.Exists(outputPath)) 143 { 144 Directory.CreateDirectory(outputPath); 145 } 146 147 var assetsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory.ToString(), "Microsoft.CmdPal.UI.ViewModels\\Assets\\template.zip"); 148 ZipFile.ExtractToDirectory(assetsPath, tempDir); 149 150 var files = Directory.GetFiles(tempDir, "*", SearchOption.AllDirectories); 151 foreach (var file in files) 152 { 153 var text = File.ReadAllText(file); 154 155 // Replace all the instances of `FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF` with a new random guid: 156 text = text.Replace("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", newGuid); 157 158 // Then replace all the `TemplateCmdPalExtension` with `extensionName` 159 text = text.Replace("TemplateCmdPalExtension", extensionName); 160 161 // Then replace all the `TemplateDisplayName` with `newDisplayName` 162 text = text.Replace("TemplateDisplayName", newDisplayName); 163 164 // We're going to write the file to the same relative location in the output path 165 var relativePath = Path.GetRelativePath(tempDir, file); 166 167 var newFileName = Path.Combine(outputPath, relativePath); 168 169 // if the file name had `TemplateCmdPalExtension` in it, replace it with `extensionName` 170 newFileName = newFileName.Replace("TemplateCmdPalExtension", extensionName); 171 172 // Make sure the directory exists 173 Directory.CreateDirectory(Path.GetDirectoryName(newFileName)!); 174 175 File.WriteAllText(newFileName, text); 176 177 // Delete the old file 178 File.Delete(file); 179 } 180 181 // Delete the temp dir 182 Directory.Delete(tempDir, true); 183 } 184 185 private string FormatJsonString(string str) => 186 187 // Escape the string for JSON 188 JsonSerializer.Serialize(str, JsonSerializationContext.Default.String); 189 }