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  }