diff --git a/AsyncSingleFileGenerator/AsyncSingleFileGeneratorSample.sln b/AsyncSingleFileGenerator/AsyncSingleFileGeneratorSample.sln new file mode 100644 index 00000000..492d80bf --- /dev/null +++ b/AsyncSingleFileGenerator/AsyncSingleFileGeneratorSample.sln @@ -0,0 +1,30 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27620.3002 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SingleFileGeneratorSample", "src\SingleFileGeneratorSample.csproj", "{154F8BFA-6D1D-4FEF-95AC-95D4222B34DA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D8854F3A-BEAA-44E0-A140-413225798789}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {154F8BFA-6D1D-4FEF-95AC-95D4222B34DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {154F8BFA-6D1D-4FEF-95AC-95D4222B34DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {154F8BFA-6D1D-4FEF-95AC-95D4222B34DA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {154F8BFA-6D1D-4FEF-95AC-95D4222B34DA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C1C1BBB7-1562-4820-8E03-795009DFE966} + EndGlobalSection +EndGlobal diff --git a/AsyncSingleFileGenerator/LICENSE b/AsyncSingleFileGenerator/LICENSE new file mode 100644 index 00000000..49d21669 --- /dev/null +++ b/AsyncSingleFileGenerator/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/AsyncSingleFileGenerator/README.md b/AsyncSingleFileGenerator/README.md new file mode 100644 index 00000000..36ddd3b2 --- /dev/null +++ b/AsyncSingleFileGenerator/README.md @@ -0,0 +1,153 @@ +# Single File Generator sample + +**Applies to Visual Studio 2017 and newer** + +This example shows how to write an Asynchronous Single File Generator in a Visual Studio extension that will output a nested file when the parent file is modified. + +> [!NOTE] +> Please note that there are two flavours of Single File Generators (aka Custom Tools) in Visual Studio: +> * Single File Generators that run synchronously on the UI thread +> * Asynchronous Single File Generators that run on a background thread (VS 2017 and newer) + +This sample demonstrates how to create an **Asynchronous Single File Generator** that is compatible with synchrouns one and tries to use async APIs where possible otherwise will fallback to sync APIs. + +To test the asynchronous behavior you will need to apply the code generator to a CPS-based project like a .NET Core project, or any SDK-style project. + +Clone the repo to test out the sample in Visual Studio 2017 yourself. + +## What is a Single File Generator +A Single File Generator is a mechanism that will auto-create and nest an output file when the source file changes. In this sample, the generator is applied to a **.js** file that will then output a **.min.js** file like so: + +![Nested file](art/code-behind.png) + +It is also known as a Custom Tool which can be manually set in the properties of supported files. + +![Property Grid](art/property-grid.png) + +The most well-known examples of existing generators are the ones creating a strongly typed C#/VB nested file for .resx files. + +Every time the file with the Custom Tool property is modified, the Single File Generator will execute to update the nested file. + +The nested file can be of any type - code, image, etc. - the sky is the limit. + +## Let's get started +Before we begin, make sure you have created a VSIX project in Visual Studio. See how to [create a VSIX project](https://docs.microsoft.com/en-us/visualstudio/extensibility/extensibility-hello-world) if you don't already have one ready. + +### Install NuGet package +The base classes for the Single File Generator are located in the [Microsoft.VisualStudio.TextTemplating.VSHost.15.0](https://www.nuget.org/packages/Microsoft.VisualStudio.TextTemplating.VSHost.15.0/) NuGet package, so go ahead and install that into your VSIX project. + +We also need the [Nuglify](https://www.nuget.org/packages/NUglify/) NuGet package that can minify JavaScript. + +### The generator +The generator is a simple class that inherits from the *BaseCodeGeneratorWithSite* and has 2 methods for us to implement. + +```c# +using Microsoft.VisualStudio.TextTemplating.VSHost; + +[Guid("82ca81c8-b507-4ba1-a33d-ff6cdad20e36")] // change this GUID +public sealed class MinifyCodeGenerator : BaseCodeGeneratorWithSite +{ + public override string GetDefaultExtension() + { + return ".min.js"; + } + + protected override byte[] GenerateCode(string inputFileName, string inputFileContent) + { + UglifyResult minified = Uglify.Js(inputFileContent); + return Encoding.UTF8.GetBytes(minified.Code); + } +} +``` + +[See full generator class in the source](src/Generators/MinifyGenerator.cs). + +That's it, you now have a Single File Generator that writes a .min.js file with the minified content of the source .js file. Now we must register the generator to make it work. + +### Registering the generator +Decorate your *Package* class with the `ProvideCodeGenerator` attribute. + +```c# +[ProvideCodeGenerator(typeof(MinifyCodeGenerator), nameof(MinifyCodeGenerator), "Minifies JavaScript", true)] +public class VSPackage : AsyncPackage +{ + ... +} +``` + +[See full Package class in the source](src/VSPackage.cs). + +> Note: if you don't have a *Package* class, add one to your project using the Add New Item dialog. The template is called *Visual Studio AsyncPackage* in VS 2017.7 + +Now the generator is registered, and you can now manually give the Custom Tool property on .js files the *MinifyCodeGenerator* value. + +That's it. We've now implemented a Single File Generator that minifies JavaScript files. + +However, it would be much easier if we give our users a command in the context-menu of files in Solution Explorer to add the value for them so they don't have to type *MinifyCodeGenerator* in the Property Grid manually. + +### Add the command button +In the .VSCT file you must specify a new button. It could look like this: + +```c# + +``` + +[See full .vsct file in the source](src/VSCommandTable.vsct). + +That will place the button in the context-menu in Solution Explorer. + +![Context Menu](art/context-menu.png) + +Then we need to add the command handler C# file. It will look similar to this: + +```c# +internal sealed class ApplyCustomTool +{ + private const int _commandId = 0x0100; + private static readonly Guid _commandSet = new Guid("4aaf93c0-70ae-4a4b-9fb6-1ad3997a9adf"); + private static DTE _dte; + + public static async Task InitializeAsync(AsyncPackage package) + { + _dte = await package.GetServiceAsync(typeof(DTE)) as DTE; + + var commandService = await package.GetServiceAsync((typeof(IMenuCommandService))) as IMenuCommandService; + var cmdId = new CommandID(_commandSet, _commandId); + var cmd = new MenuCommand(OnExecute, cmdId) + commandService.AddCommand(cmd); + } + + private static void OnExecute(object sender, EventArgs e) + { + ProjectItem item = _dte.SelectedItems.Item(1).ProjectItem; + item.Properties.Item("CustomTool").Value = nameof(MinifyCodeGenerator); + } +} +``` + +[See full command handler in the source](src/Commands/ApplyCustomTool.cs). + +And then finally initialize the command handler from the *Package* initialization method. + +```c# +protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) +{ + await ApplyCustomTool.InitializeAsync(this); +} +``` + +[See full Package class in the source](src/VSPackage.cs). + +### Single File Generators in the wild +Here are more samples of open source extensions implementing Single File Generators. + +* [VSIX Synchronizer](https://github.com/madskristensen/VsixSynchronizer) +* [Extensibility Tools](https://github.com/madskristensen/extensibilitytools) + +## License +[Apache 2.0](LICENSE) \ No newline at end of file diff --git a/AsyncSingleFileGenerator/art/code-behind.png b/AsyncSingleFileGenerator/art/code-behind.png new file mode 100644 index 00000000..2e52dd0b Binary files /dev/null and b/AsyncSingleFileGenerator/art/code-behind.png differ diff --git a/AsyncSingleFileGenerator/art/context-menu.png b/AsyncSingleFileGenerator/art/context-menu.png new file mode 100644 index 00000000..f977f1b0 Binary files /dev/null and b/AsyncSingleFileGenerator/art/context-menu.png differ diff --git a/AsyncSingleFileGenerator/art/property-grid.png b/AsyncSingleFileGenerator/art/property-grid.png new file mode 100644 index 00000000..898e1950 Binary files /dev/null and b/AsyncSingleFileGenerator/art/property-grid.png differ diff --git a/AsyncSingleFileGenerator/src/Commands/ApplyCustomTool.cs b/AsyncSingleFileGenerator/src/Commands/ApplyCustomTool.cs new file mode 100644 index 00000000..3bfc450d --- /dev/null +++ b/AsyncSingleFileGenerator/src/Commands/ApplyCustomTool.cs @@ -0,0 +1,44 @@ +namespace AsyncSingleFileGeneratorSample; + +using System; +using System.ComponentModel.Design; +using System.IO; +using Microsoft.VisualStudio.Shell; +using Task = System.Threading.Tasks.Task; +using Microsoft.VisualStudio.Interop; +using EnvDTE; + +internal sealed class ApplyCustomTool +{ + private const int _commandId = 0x0100; + private static readonly Guid _commandSet = new Guid("4aaf93c0-70ae-4a4b-9fb6-1ad3997a9adf"); + private static DTE _dte; + + public static async Task InitializeAsync(AsyncPackage package) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + _dte = await package.GetServiceAsync(typeof(DTE)) as DTE; + + var commandService = await package.GetServiceAsync((typeof(IMenuCommandService))) as IMenuCommandService; + var cmdId = new CommandID(_commandSet, _commandId); + + var cmd = new OleMenuCommand(OnExecute, cmdId) + { + // This will defer visibility control to the VisibilityConstraints section in the .vsct file + Supported = false + }; + + commandService.AddCommand(cmd); + } + + private static void OnExecute(object sender, EventArgs e) + { + ProjectItem item = _dte.SelectedItems.Item(1).ProjectItem; + + if (item != null) + { + item.Properties.Item("CustomTool").Value = MinifyCodeGenerator.Name; + } + } +} diff --git a/AsyncSingleFileGenerator/src/Generators/BaseCodeGeneratorWithSiteAsync.cs b/AsyncSingleFileGenerator/src/Generators/BaseCodeGeneratorWithSiteAsync.cs new file mode 100644 index 00000000..d94f2a0c --- /dev/null +++ b/AsyncSingleFileGenerator/src/Generators/BaseCodeGeneratorWithSiteAsync.cs @@ -0,0 +1,209 @@ +namespace AsyncSingleFileGeneratorSample; + +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.OLE.Interop; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Generators; +using Microsoft.VisualStudio.Shell.Interop; +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using ServiceProvider = Microsoft.VisualStudio.Shell.ServiceProvider; + +public abstract class BaseCodeGeneratorWithSiteAsync : IObjectWithSite, IDisposable, IVsSingleFileGenerator, IVsSingleFileGeneratorAsync +{ + private object site; + private ServiceProvider serviceProvider; + private ServiceProvider globalProvider; + private bool _disposed; + + /// + /// Get a wrapper on the containing project system's Service provider + /// + /// + /// This is a limited service provider that can only reliably provide VxDTE::SID_SVSProjectItem + /// SID_SVSWebReferenceDynamicProperties IID_IVsHierarchy SID_SVsApplicationSettings + /// To get the global provider, call GetSite on IVSHierarchy or use the GlobalServiceProvider + /// property + /// + protected ServiceProvider SiteServiceProvider + { + get + { + ThreadHelper.ThrowIfNotOnUIThread(); + if (serviceProvider == null) + { + var sp = site as Microsoft.VisualStudio.OLE.Interop.IServiceProvider; + serviceProvider = new ServiceProvider(sp); + } + + return serviceProvider; + } + } + + /// + /// Provides a wrapper on the global service provider for Visual Studio + /// + protected ServiceProvider GlobalServiceProvider + { + get + { + ThreadHelper.ThrowIfNotOnUIThread(); + if (globalProvider == null) + { + var siteServiceProvider = SiteServiceProvider; + if (siteServiceProvider != null && siteServiceProvider.GetService(typeof(IVsHierarchy)) is IVsHierarchy vsHierarchy) + { + ErrorHandler.ThrowOnFailure(vsHierarchy.GetSite(out var ppSP)); + if (ppSP != null) + { + globalProvider = new ServiceProvider(ppSP); + } + } + } + + return globalProvider; + } + } + + /// + /// SetSite method of IOleObjectWithSite + /// + /// site for this object to use + public virtual void SetSite(object pUnkSite) + { + site = pUnkSite; + serviceProvider = null; + } + + /// + /// GetSite method of IOleObjectWithSite + /// + /// interface to get + /// array in which to stuff return value + public virtual void GetSite(ref Guid riid, out IntPtr ppvSite) + { + if (site == null) + { + Marshal.ThrowExceptionForHR(-2147467259); + } + + IntPtr iUnknownForObject = Marshal.GetIUnknownForObject(site); + Marshal.QueryInterface(iUnknownForObject, ref riid, out var ppv); + if (ppv == IntPtr.Zero) + { + Marshal.ThrowExceptionForHR(-2147467262); + } + + ppvSite = ppv; + } + + protected object GetService(Guid service) => SiteServiceProvider?.GetService(service); + + protected object GetService(Type service) => SiteServiceProvider?.GetService(service); + + int IVsSingleFileGenerator.DefaultExtension(out string pbstrDefaultExtension) + { + pbstrDefaultExtension = GetDefaultExtension(); + return VSConstants.S_OK; + } + + int IVsSingleFileGenerator.Generate(string wszInputFilePath, string bstrInputFileContents, string wszDefaultNamespace, IntPtr[] rgbOutputFileContents, out uint pcbOutput, IVsGeneratorProgress pGenerateProgress) + { + try + { + IntPtr outputFileContents = IntPtr.Zero; + GenerateInternal(wszInputFilePath, bstrInputFileContents, wszDefaultNamespace, out outputFileContents, out var output, pGenerateProgress); + rgbOutputFileContents[0] = outputFileContents; + pcbOutput = (uint)output; + } + catch + { + pcbOutput = 0u; + rgbOutputFileContents[0] = IntPtr.Zero; + return VSConstants.E_FAIL; + } + + return VSConstants.S_OK; + } + + protected abstract string GetDefaultExtension(); + + private void GenerateInternal( + string inputFilePath, string inputFileContents, string defaultNamespace, + out IntPtr outputFileContents, out int output, IVsGeneratorProgress pGenerateProgress) + { + if (inputFileContents == null) + { + throw new ArgumentNullException(nameof(inputFileContents)); + } + + var array = ThreadHelper.JoinableTaskFactory.Run( + async () => await GenerateCodeAsync(inputFilePath, inputFileContents, defaultNamespace, pGenerateProgress)); + if (array == null) + { + outputFileContents = IntPtr.Zero; + output = 0; + } + else + { + output = array.Length; + outputFileContents = Marshal.AllocCoTaskMem(output); + Marshal.Copy(array, 0, outputFileContents, output); + } + } + + async Task IVsSingleFileGeneratorAsync.GetDefaultExtensionAsync(CancellationToken cancellationToken) + => GetDefaultExtension(); + + async Task IVsSingleFileGeneratorAsync.GenerateAsync(string inputFilePath, string inputFileContents, string defaultNamespace, Stream outputStream, IVsGeneratorProgress generatorProgress, CancellationToken cancellationToken) + { + var resultBytes = await GenerateCodeAsync(inputFilePath, inputFileContents, defaultNamespace, generatorProgress, cancellationToken); + if (resultBytes != null) + { + await outputStream.WriteAsync(resultBytes, 0, resultBytes.Length); + } + + return GeneratorResult.Success; + } + + protected abstract Task GenerateCodeAsync( + string inputFilePath, string inputFileContents, string defaultNamespace, + IVsGeneratorProgress generateProgress, CancellationToken cancellationToken = default); + + ~BaseCodeGeneratorWithSiteAsync() => Dispose(disposing: false); + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// In derived classes, override Dispose(bool disposing) and make sure you call this method + /// even in case of exceptions (in finally). + /// + /// True to release managed resources; false from finalizer. + protected virtual void Dispose(bool disposing) + { + if (_disposed) return; + + try + { + if (disposing) + { + serviceProvider?.Dispose(); + serviceProvider = null; + + globalProvider?.Dispose(); + globalProvider = null; + } + } + finally + { + _disposed = true; + } + } +} diff --git a/AsyncSingleFileGenerator/src/Generators/MinifyGenerator.cs b/AsyncSingleFileGenerator/src/Generators/MinifyGenerator.cs new file mode 100644 index 00000000..bb6b2b89 --- /dev/null +++ b/AsyncSingleFileGenerator/src/Generators/MinifyGenerator.cs @@ -0,0 +1,70 @@ +namespace AsyncSingleFileGeneratorSample; + +using EnvDTE; +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.TextTemplating.VSHost; +using NUglify; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +/// +/// Most of the magic is done in the BaseCodeGeneratorWithSiteAsync class, don't forget to check it out! +/// +[Guid("AE47AF68-696D-491E-B536-32F859E0B988")] +public sealed class MinifyCodeGenerator : BaseCodeGeneratorWithSiteAsync +{ + public const string Name = nameof(MinifyCodeGenerator); + public const string Description = "Generates a minified version of JavaScript, CSS and HTML files files."; + + protected override string GetDefaultExtension() + { + var item = GetService(typeof(ProjectItem)) as ProjectItem; + return ".min" + Path.GetExtension(item?.FileNames[1]); + } + + protected override async Task GenerateCodeAsync( + string inputFileName, string inputFileContent, string defaultNamespace, + IVsGeneratorProgress generateProgress, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(inputFileContent)) { return null; } + + var result = await MinifyAsync(inputFileName, inputFileContent); + + if (result.HasErrors) + { + return Encoding.UTF8.GetBytes("// Source file contains errors"); + } + else + { + return Encoding.UTF8.GetBytes(result.Code); + } + } + + private static Task MinifyAsync(string inputFileName, string inputFileContent) + { + // Note! Uglify does not have async methods, so we run it in a Task to avoid blocking. + + return Task.Run(() => + { + string ext = Path.GetExtension(inputFileName).ToLowerInvariant(); + + switch (ext) + { + case ".js": + return Uglify.Js(inputFileContent); + case ".css": + return Uglify.Css(inputFileContent); + case ".htm": + case ".html": + return Uglify.Html(inputFileContent); + } + + return new UglifyResult(inputFileContent, new List()); + }); + } +} \ No newline at end of file diff --git a/AsyncSingleFileGenerator/src/Properties/AssemblyInfo.cs b/AsyncSingleFileGenerator/src/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..ddaa6ae2 --- /dev/null +++ b/AsyncSingleFileGenerator/src/Properties/AssemblyInfo.cs @@ -0,0 +1,16 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("Async Single File Generator Sample")] +[assembly: AssemblyDescription("Shows how to implement an async single file generator for Visual Studio")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Reza Niroomand")] +[assembly: AssemblyProduct("Async Single File Generator Sample")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/AsyncSingleFileGenerator/src/Resources/ApplyCustomToolPackage.ico b/AsyncSingleFileGenerator/src/Resources/ApplyCustomToolPackage.ico new file mode 100644 index 00000000..d323b07f Binary files /dev/null and b/AsyncSingleFileGenerator/src/Resources/ApplyCustomToolPackage.ico differ diff --git a/AsyncSingleFileGenerator/src/Resources/Icon128x128.png b/AsyncSingleFileGenerator/src/Resources/Icon128x128.png new file mode 100644 index 00000000..76e4085f Binary files /dev/null and b/AsyncSingleFileGenerator/src/Resources/Icon128x128.png differ diff --git a/AsyncSingleFileGenerator/src/SingleFileGeneratorSample.csproj b/AsyncSingleFileGenerator/src/SingleFileGeneratorSample.csproj new file mode 100644 index 00000000..fc2fe289 --- /dev/null +++ b/AsyncSingleFileGenerator/src/SingleFileGeneratorSample.csproj @@ -0,0 +1,128 @@ + + + + 15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + true + 10.0 + + + + false + + + + + + + + Debug + AnyCPU + 2.0 + {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + {154F8BFA-6D1D-4FEF-95AC-95D4222B34DA} + Library + Properties + AsyncSingleFileGeneratorSample + AsyncSingleFileGeneratorSample + v4.8 + true + true + true + true + true + false + Program + $(DevEnvDir)devenv.exe + /rootsuffix Exp + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + Designer + + + + + + + + + + + + + + + + + + + + Menus.ctmenu + + + + + true + VSPackage + + + + + + true + Always + + + + + 17.14.40265 + + + 17.14.2120 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + 13.0.4 + + + 1.21.17 + + + + + + \ No newline at end of file diff --git a/AsyncSingleFileGenerator/src/VSCommandTable.vsct b/AsyncSingleFileGenerator/src/VSCommandTable.vsct new file mode 100644 index 00000000..b7494fec --- /dev/null +++ b/AsyncSingleFileGenerator/src/VSCommandTable.vsct @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AsyncSingleFileGenerator/src/VSPackage.cs b/AsyncSingleFileGenerator/src/VSPackage.cs new file mode 100644 index 00000000..6aac96e3 --- /dev/null +++ b/AsyncSingleFileGenerator/src/VSPackage.cs @@ -0,0 +1,27 @@ +namespace AsyncSingleFileGeneratorSample; + +using System; +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.TextTemplating.VSHost; +using Task = System.Threading.Tasks.Task; + +[Guid("2e927fa3-8684-47fc-9674-0046499860d3")] // Must match the GUID in the .vsct file +[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] +[InstalledProductRegistration("Single File Generator Sample", "", "1.0")] +[ProvideMenuResource("Menus.ctmenu", 1)] +[ProvideCodeGenerator(typeof(MinifyCodeGenerator), MinifyCodeGenerator.Name, MinifyCodeGenerator.Description, true)] +[ProvideUIContextRule("69760bd3-80f0-4901-818d-c4656aaa08e9", // Must match the GUID in the .vsct file + name: "UI Context", + expression: "js | css | html", // This will make the button only show on .js, .css and .htm(l) files + termNames: new[] { "js", "css", "html" }, + termValues: new[] { "HierSingleSelectionName:.js$", "HierSingleSelectionName:.css$", "HierSingleSelectionName:.html?$" })] +public sealed class VSPackage : AsyncPackage +{ + protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) + { + await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + await ApplyCustomTool.InitializeAsync(this); + } +} diff --git a/AsyncSingleFileGenerator/src/VSPackage.resx b/AsyncSingleFileGenerator/src/VSPackage.resx new file mode 100644 index 00000000..8d8bd213 --- /dev/null +++ b/AsyncSingleFileGenerator/src/VSPackage.resx @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ApplyCustomTool Extension + + + ApplyCustomTool Visual Studio Extension Detailed Info + + + Resources\ApplyCustomToolPackage.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/AsyncSingleFileGenerator/src/source.extension.vsixmanifest b/AsyncSingleFileGenerator/src/source.extension.vsixmanifest new file mode 100644 index 00000000..d790c55a --- /dev/null +++ b/AsyncSingleFileGenerator/src/source.extension.vsixmanifest @@ -0,0 +1,23 @@ + + + + + Single File Generator Sample + Shows how to implement a single file generator for Visual Studio + Resources\Icon128x128.png + Resources\Icon128x128.png + + + + + + + + + + + + + + +