diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca402fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +## Ignore Visual Studio temporary files, build and test results + +*.suo +*.user +*.cache +*.vspscc + +[Bb]in/ +[Oo]bj/ +[Tt]est[Rr]esult*/ +[Pp]ackages/ \ No newline at end of file diff --git a/.nuget/NuGet.Config b/.nuget/NuGet.Config new file mode 100644 index 0000000..6a318ad --- /dev/null +++ b/.nuget/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CodeAnalysisDictionary.xml b/CodeAnalysisDictionary.xml new file mode 100644 index 0000000..44dda82 --- /dev/null +++ b/CodeAnalysisDictionary.xml @@ -0,0 +1,10 @@ + + + + Lex + LLoc + EOF + Polymorphically + + + \ No newline at end of file diff --git a/CommonAssemblyInfo.cs b/CommonAssemblyInfo.cs new file mode 100644 index 0000000..93d84cb --- /dev/null +++ b/CommonAssemblyInfo.cs @@ -0,0 +1,37 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +#if DEBUG +[assembly: AssemblyConfiguration("Debug")] +#else +[assembly: AssemblyConfiguration("Release")] +#endif + +[assembly: AssemblyProduct(T4Toolbox.AssemblyInfo.Product)] +[assembly: AssemblyDescription(T4Toolbox.AssemblyInfo.Description)] +[assembly: AssemblyCompany("Oleg Sych")] +[assembly: AssemblyCopyright("Copyright © Oleg Sych. All Rights Reserved.")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: AssemblyVersion(T4Toolbox.AssemblyInfo.Version)] +[assembly: AssemblyFileVersion(T4Toolbox.AssemblyInfo.Version)] +[assembly: ComVisible(false)] +[assembly: NeutralResourcesLanguage("en-US")] + +// Allow all projects in this solution to access each-other's internals by default. +// In many instances, we need this to enable testing as well as to access constants +// in T4Toolbox.AssemblyInfo class. Revisit this decision when the number of assemblies +// in the project increases to the point where limiting access to internals within the +// solution becomes beneficial. +[assembly: InternalsVisibleTo("T4Toolbox.DirectiveProcessors")] +[assembly: InternalsVisibleTo("T4Toolbox.Tests")] +[assembly: InternalsVisibleTo("T4Toolbox.VisualStudio")] +[assembly: InternalsVisibleTo("T4Toolbox.VisualStudio.IntegrationTests")] +[assembly: InternalsVisibleTo("T4Toolbox.VisualStudio.Tests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/LICENSE b/LICENSE index 5b3aa2f..15f0b1c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Oleg Sych +Copyright (c) 2008 Oleg Sych Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/LocalTestRun.testrunconfig b/LocalTestRun.testrunconfig new file mode 100644 index 0000000..c9b4e36 --- /dev/null +++ b/LocalTestRun.testrunconfig @@ -0,0 +1,9 @@ + + + Test settings required to run Visual Studio integration tests. + + + + + + \ No newline at end of file diff --git a/Settings.StyleCop b/Settings.StyleCop new file mode 100644 index 0000000..ea32c2c --- /dev/null +++ b/Settings.StyleCop @@ -0,0 +1,91 @@ + + + + autogenerated + int + remoting + templating + tt + Intelli + + NoMerge + + + + + False + + \.g\.cs$ + \.generated\.cs$ + \.g\.i\.cs$ + TemporaryGeneratedFile_.*\.cs$ + + + + + + + + + + True + + + + + True + + + + + True + + + + + False + + + + + False + + + + + False + + + + + False + + + + + Oleg Sych + Copyright © Oleg Sych. All Rights Reserved. + + + + + + + False + + + + + + + + + if + is + on + to + + + + + \ No newline at end of file diff --git a/T4Toolbox.Common.props b/T4Toolbox.Common.props new file mode 100644 index 0000000..2713e66 --- /dev/null +++ b/T4Toolbox.Common.props @@ -0,0 +1,34 @@ + + + + Debug + AnyCPU + AnyCPU + v4.5 + + true + full + prompt + 4 + 512 + true + bin\$(Configuration)\ + + + + + $(OutputPath)$(AssemblyName).xml + + + true + $(MSBuildThisFileDirectory)\T4Toolbox.Common.ruleset + + + false + false + + \ No newline at end of file diff --git a/T4Toolbox.Common.ruleset b/T4Toolbox.Common.ruleset new file mode 100644 index 0000000..9d5771e --- /dev/null +++ b/T4Toolbox.Common.ruleset @@ -0,0 +1,593 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/T4Toolbox.sln b/T4Toolbox.sln new file mode 100644 index 0000000..881d8b6 --- /dev/null +++ b/T4Toolbox.sln @@ -0,0 +1,98 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.31101.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{42F21B27-CBA5-4FB6-A855-8B74F05F56CD}" + ProjectSection(SolutionItems) = preProject + CodeAnalysisDictionary.xml = CodeAnalysisDictionary.xml + CommonAssemblyInfo.cs = CommonAssemblyInfo.cs + LocalTestRun.testrunconfig = LocalTestRun.testrunconfig + Settings.StyleCop = Settings.StyleCop + T4Toolbox.Common.props = T4Toolbox.Common.props + T4Toolbox.Common.ruleset = T4Toolbox.Common.ruleset + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3594248C-C8A9-4EAB-8E63-0914300D4ECE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "T4Toolbox", "src\T4Toolbox\T4Toolbox.csproj", "{682E771A-76F7-4972-BBDC-1250B67F399B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "T4Toolbox.VisualStudio", "src\T4Toolbox.VisualStudio\T4Toolbox.VisualStudio.csproj", "{1E1E9161-CBE4-4538-928C-539AA5E70153}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{994DFDA5-97F7-4688-A2FA-6D8E7AB2BC18}" + ProjectSection(SolutionItems) = preProject + test\Settings.StyleCop = test\Settings.StyleCop + test\T4Toolbox.Tests.props = test\T4Toolbox.Tests.props + test\T4Toolbox.Tests.ruleset = test\T4Toolbox.Tests.ruleset + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "T4Toolbox.Tests", "test\T4Toolbox.Tests\T4Toolbox.Tests.csproj", "{2A05BF5E-B2B2-4222-91A3-BB86AE8A94CE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "T4Toolbox.VisualStudio.IntegrationTests", "test\T4Toolbox.VisualStudio.IntegrationTests\T4Toolbox.VisualStudio.IntegrationTests.csproj", "{846B29AB-AAA2-4080-B4B4-A440948CC61A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "T4Toolbox.VisualStudio.Tests", "test\T4Toolbox.VisualStudio.Tests\T4Toolbox.VisualStudio.Tests.csproj", "{7CBACA4C-728A-4818-839C-E22C24677AFA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "T4Toolbox.DirectiveProcessors", "src\T4Toolbox.DirectiveProcessors\T4Toolbox.DirectiveProcessors.csproj", "{E0282961-2D83-48CC-B4D4-8257449CF8F7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "T4Toolbox.VisualStudio.ItemTemplates", "src\T4Toolbox.VisualStudio.ItemTemplates\T4Toolbox.VisualStudio.ItemTemplates.csproj", "{EA04B345-97BE-4A49-9C9C-3EBD4F5D2250}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "T4Toolbox.vsix", "src\T4Toolbox.vsix\T4Toolbox.vsix.csproj", "{8E492B04-AF03-4A88-9A5D-D34D2386A4E5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{71EF8BD3-A665-4304-8F9D-D6A849A9830D}" + ProjectSection(SolutionItems) = preProject + .nuget\NuGet.Config = .nuget\NuGet.Config + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {682E771A-76F7-4972-BBDC-1250B67F399B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {682E771A-76F7-4972-BBDC-1250B67F399B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {682E771A-76F7-4972-BBDC-1250B67F399B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {682E771A-76F7-4972-BBDC-1250B67F399B}.Release|Any CPU.Build.0 = Release|Any CPU + {1E1E9161-CBE4-4538-928C-539AA5E70153}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E1E9161-CBE4-4538-928C-539AA5E70153}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E1E9161-CBE4-4538-928C-539AA5E70153}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E1E9161-CBE4-4538-928C-539AA5E70153}.Release|Any CPU.Build.0 = Release|Any CPU + {2A05BF5E-B2B2-4222-91A3-BB86AE8A94CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A05BF5E-B2B2-4222-91A3-BB86AE8A94CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A05BF5E-B2B2-4222-91A3-BB86AE8A94CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A05BF5E-B2B2-4222-91A3-BB86AE8A94CE}.Release|Any CPU.Build.0 = Release|Any CPU + {846B29AB-AAA2-4080-B4B4-A440948CC61A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {846B29AB-AAA2-4080-B4B4-A440948CC61A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {846B29AB-AAA2-4080-B4B4-A440948CC61A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {846B29AB-AAA2-4080-B4B4-A440948CC61A}.Release|Any CPU.Build.0 = Release|Any CPU + {7CBACA4C-728A-4818-839C-E22C24677AFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7CBACA4C-728A-4818-839C-E22C24677AFA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7CBACA4C-728A-4818-839C-E22C24677AFA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7CBACA4C-728A-4818-839C-E22C24677AFA}.Release|Any CPU.Build.0 = Release|Any CPU + {E0282961-2D83-48CC-B4D4-8257449CF8F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0282961-2D83-48CC-B4D4-8257449CF8F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0282961-2D83-48CC-B4D4-8257449CF8F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0282961-2D83-48CC-B4D4-8257449CF8F7}.Release|Any CPU.Build.0 = Release|Any CPU + {EA04B345-97BE-4A49-9C9C-3EBD4F5D2250}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA04B345-97BE-4A49-9C9C-3EBD4F5D2250}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA04B345-97BE-4A49-9C9C-3EBD4F5D2250}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA04B345-97BE-4A49-9C9C-3EBD4F5D2250}.Release|Any CPU.Build.0 = Release|Any CPU + {8E492B04-AF03-4A88-9A5D-D34D2386A4E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8E492B04-AF03-4A88-9A5D-D34D2386A4E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8E492B04-AF03-4A88-9A5D-D34D2386A4E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8E492B04-AF03-4A88-9A5D-D34D2386A4E5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {682E771A-76F7-4972-BBDC-1250B67F399B} = {3594248C-C8A9-4EAB-8E63-0914300D4ECE} + {1E1E9161-CBE4-4538-928C-539AA5E70153} = {3594248C-C8A9-4EAB-8E63-0914300D4ECE} + {2A05BF5E-B2B2-4222-91A3-BB86AE8A94CE} = {994DFDA5-97F7-4688-A2FA-6D8E7AB2BC18} + {846B29AB-AAA2-4080-B4B4-A440948CC61A} = {994DFDA5-97F7-4688-A2FA-6D8E7AB2BC18} + {7CBACA4C-728A-4818-839C-E22C24677AFA} = {994DFDA5-97F7-4688-A2FA-6D8E7AB2BC18} + {E0282961-2D83-48CC-B4D4-8257449CF8F7} = {3594248C-C8A9-4EAB-8E63-0914300D4ECE} + {EA04B345-97BE-4A49-9C9C-3EBD4F5D2250} = {3594248C-C8A9-4EAB-8E63-0914300D4ECE} + {8E492B04-AF03-4A88-9A5D-D34D2386A4E5} = {3594248C-C8A9-4EAB-8E63-0914300D4ECE} + EndGlobalSection +EndGlobal diff --git a/src/T4Toolbox.DirectiveProcessors/AssemblyInfo.cs b/src/T4Toolbox.DirectiveProcessors/AssemblyInfo.cs new file mode 100644 index 0000000..5b24072 --- /dev/null +++ b/src/T4Toolbox.DirectiveProcessors/AssemblyInfo.cs @@ -0,0 +1,21 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +using System; + +[assembly: CLSCompliant(false)] + +namespace T4Toolbox.DirectiveProcessors +{ + /// + /// Defines constants describing the T4Toolbox.VisualStudio assembly. + /// + internal abstract class AssemblyInfo : T4Toolbox.AssemblyInfo + { + /// + /// Gets the name of the assembly. + /// + public new const string Name = "T4Toolbox.DirectiveProcessors"; + } +} \ No newline at end of file diff --git a/src/T4Toolbox.DirectiveProcessors/DirectiveProcessor.cs b/src/T4Toolbox.DirectiveProcessors/DirectiveProcessor.cs new file mode 100644 index 0000000..541bff8 --- /dev/null +++ b/src/T4Toolbox.DirectiveProcessors/DirectiveProcessor.cs @@ -0,0 +1,420 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.DirectiveProcessors +{ + using System; + using System.CodeDom; + using System.CodeDom.Compiler; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Linq; + using Microsoft.VisualStudio.TextTemplating; + + /// + /// Base class for directive processors. + /// + public abstract class DirectiveProcessor : IDirectiveProcessor, IDisposable + { + /// + /// Gets the collection to which the directive processor can add errors and warnings. + /// + /// A instance. + public CompilerErrorCollection Errors { get; private set; } + + /// + /// Gets or sets a value indicating whether the directive processor requires the run to be host-specific. + /// + /// + /// True if the directive processor requires a host-specific processing run. + /// + public bool RequiresProcessingRunIsHostSpecific { get; protected set; } + + /// + /// Gets a T4 engine host. + /// + /// + /// A object obtained by the method. + /// + protected ITextTemplatingEngineHost Host { get; private set; } + + /// + /// Gets a collection of attribute declarations that will be applied to the generated transformation class. + /// + /// + /// A collection of objects. + /// + protected CodeAttributeDeclarationCollection ClassAttributes { get; private set; } + + /// + /// Gets the class code buffer. + /// + /// + /// A object that serves as a buffer for generated code. + /// + protected StringWriter ClassCode { get; private set; } + + /// + /// Gets the directive name. + /// + /// A that contains directive name. + /// + /// Override this property in the derived class to indicate which directive + /// the processor will handle. + /// + protected abstract string DirectiveName { get; } + + /// + /// Gets dispose code buffer. + /// + /// + /// A object that serves as a buffer for generated code. + /// + protected StringWriter DisposeCode { get; private set; } + + /// + /// Gets the namespace imports buffer. + /// + /// + /// A of namespaces. + /// + protected ICollection Imports { get; private set; } + + /// + /// Gets the post-initialization code buffer. + /// + /// + /// A object that serves as a buffer for generated code. + /// + protected StringWriter PostInitializationCode { get; private set; } + + /// + /// Gets the pre-initialization code buffer. + /// + /// + /// A object that serves as a buffer for generated code. + /// + protected StringWriter PreInitializationCode { get; private set; } + + /// + /// Gets a value indicating whether the current processing run is host-specific. + /// + /// + /// True, if the current processing run is host-specific. + /// + protected bool ProcessingRunIsHostSpecific { get; private set; } + + /// + /// Gets the assembly references buffer. + /// + /// + /// A of assembly references. + /// + protected ICollection References { get; private set; } + + /// + /// Gets a language provider. + /// + /// + /// A object obtained by the method. + /// + protected CodeDomProvider LanguageProvider { get; private set; } + + /// + /// Releases disposable resources owned by the directive processor. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + #region IDirectiveProcessor + + /// + /// Notifies the directive processor that the processing run is finished. + /// + public void FinishProcessingRun() + { + if (this.DisposeCode.GetStringBuilder().Length > 0) + { + this.GenerateDisposeMethod(); + } + + // Release external references received from T4 + this.LanguageProvider = null; + + // DO NOT release the internal buffers. T4 Engine accesses them between the runs. + } + + /// + /// This method is not used and returns an empty string. + /// + /// + /// A that contains the code to add to the generated + /// transformation class. + /// + public string GetClassCodeForProcessingRun() + { + return this.ClassCode.ToString(); + } + + /// + /// This method is not used and returns an empty array. + /// + /// + /// An array of type that contains the namespaces. + /// + public string[] GetImportsForProcessingRun() + { + return this.Imports.Distinct().ToArray(); + } + + /// + /// Gets a collection of custom attribute declarations to add to the generated template class. + /// + /// . + public CodeAttributeDeclarationCollection GetTemplateClassCustomAttributes() + { + return this.ClassAttributes; + } + + /// + /// This method is not used and returns an empty string. + /// + /// + /// A that contains the code to add to the generated + /// transformation class. + /// + public string GetPostInitializationCodeForProcessingRun() + { + return this.PostInitializationCode.ToString(); + } + + /// + /// This method is not used and returns an empty string. + /// + /// + /// A that contains the code to add to the generated + /// transformation class. + /// + public string GetPreInitializationCodeForProcessingRun() + { + return this.PreInitializationCode.ToString(); + } + + /// + /// This method is not used and returns an empty array. + /// + /// + /// An array of type that contains the references. + /// + public string[] GetReferencesForProcessingRun() + { + return this.References.Distinct().ToArray(); + } + + /// + /// T4 engine calls this method in the beginning of template transformation. + /// + /// + /// The object hosting the transformation. + /// + public void Initialize(ITextTemplatingEngineHost host) + { + this.Host = host; + } + + /// + /// Returns true when is "t4toolbox". + /// + /// Name of the directive. + /// + /// true if the directive is supported by the processor; otherwise, false. + /// + public bool IsDirectiveSupported(string directiveName) + { + if (directiveName == null) + { + throw new ArgumentNullException("directiveName"); + } + + return string.Compare(this.DirectiveName, directiveName, StringComparison.OrdinalIgnoreCase) == 0; + } + + /// + /// This method is not used and left blank. + /// + /// + /// The name of the directive to process. + /// + /// + /// The arguments for the directive. + /// + public void ProcessDirective(string directiveName, IDictionary arguments) + { + // Validate directiveName argument + if (!this.IsDirectiveSupported(directiveName)) + { + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + "Unsupported directive name: '{0}'. Please use '{1}' instead.", + directiveName, + this.DirectiveName), + "directiveName"); + } + + // Validate arguments argument + if (arguments == null) + { + throw new ArgumentNullException("arguments"); + } + + this.Process(arguments); + } + + /// + /// Informs the directive processor whether the run is host-specific. + /// + /// True, if the processing run is host-specific. + public void SetProcessingRunIsHostSpecific(bool hostSpecific) + { + this.ProcessingRunIsHostSpecific = hostSpecific; + } + + /// + /// Begins a round of directive processing. + /// + /// CodeDom language provider for generating code. + /// Contents of the T4 template. + /// Compiler Errors. + public void StartProcessingRun(CodeDomProvider languageProvider, string templateContents, CompilerErrorCollection errors) + { + // Validate parameters + if (languageProvider == null) + { + throw new ArgumentNullException("languageProvider"); + } + + if (errors == null) + { + throw new ArgumentNullException("errors"); + } + + // Initialize references to external objects provided by T4 + this.LanguageProvider = languageProvider; + this.Errors = errors; + + // Clean up buffers here instead of FinishProcessingRun because T4 uses them between the runs + this.CleanupBuffers(); + this.InitializeBuffers(); + } + + #endregion + + /// + /// Disposes managed resources owned by this directive processor. + /// + /// + /// This parameter is always true. It is provided for consistency with pattern. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + this.CleanupBuffers(); + } + } + + /// + /// Processes the directive. + /// + /// + /// A dictionary of arguments specified for the directive in the template. + /// + protected abstract void Process(IDictionary arguments); + + /// + /// Reports a warning with the specified . + /// + protected void Warning(string message) + { + if (message == null) + { + throw new ArgumentNullException("message"); + } + + this.Errors.Add(new CompilerError { ErrorText = message, FileName = this.Host.TemplateFile, IsWarning = true }); + } + + private void CleanupBuffers() + { + this.ClassAttributes = null; + this.Imports = null; + this.References = null; + + if (this.ClassCode != null) + { + this.ClassCode.Dispose(); + this.ClassCode = null; + } + + if (this.DisposeCode != null) + { + this.DisposeCode.Dispose(); + this.DisposeCode = null; + } + + if (this.PostInitializationCode != null) + { + this.PostInitializationCode.Dispose(); + this.PostInitializationCode = null; + } + + if (this.PreInitializationCode != null) + { + this.PreInitializationCode.Dispose(); + this.PreInitializationCode = null; + } + } + + private void GenerateDisposeMethod() + { + //// protected override void Dispose(bool disposing) { + var disposeMethod = new CodeMemberMethod { Name = "Dispose", Attributes = MemberAttributes.Family | MemberAttributes.Override }; + var disposingArgument = new CodeParameterDeclarationExpression(new CodeTypeReference(typeof(bool)), "disposing"); + disposeMethod.Parameters.Add(disposingArgument); + + //// base.Dispose(disposing); + disposeMethod.Statements.Add(new CodeMethodInvokeExpression( + new CodeBaseReferenceExpression(), + "Dispose", + new CodeArgumentReferenceExpression(disposingArgument.Name))); + + //// if (disposing) { + //// // DisposeCode + disposeMethod.Statements.Add(new CodeConditionStatement( + new CodeArgumentReferenceExpression(disposingArgument.Name), + new CodeSnippetStatement(this.DisposeCode.ToString()))); + + //// } + //// } + this.LanguageProvider.GenerateCodeFromMember(disposeMethod, this.ClassCode, null); + } + + private void InitializeBuffers() + { + this.ClassAttributes = new CodeAttributeDeclarationCollection(); + this.Imports = new List(); + this.References = new List(); + + this.ClassCode = new StringWriter(CultureInfo.InvariantCulture); + this.DisposeCode = new StringWriter(CultureInfo.InvariantCulture); + this.PostInitializationCode = new StringWriter(CultureInfo.InvariantCulture); + this.PreInitializationCode = new StringWriter(CultureInfo.InvariantCulture); + } + } +} diff --git a/src/T4Toolbox.DirectiveProcessors/T4Toolbox.DirectiveProcessors.csproj b/src/T4Toolbox.DirectiveProcessors/T4Toolbox.DirectiveProcessors.csproj new file mode 100644 index 0000000..3e8b13e --- /dev/null +++ b/src/T4Toolbox.DirectiveProcessors/T4Toolbox.DirectiveProcessors.csproj @@ -0,0 +1,58 @@ + + + + 10.0.20506 + 2.0 + {E0282961-2D83-48CC-B4D4-8257449CF8F7} + Library + Properties + T4Toolbox.DirectiveProcessors + T4Toolbox.DirectiveProcessors + 3fedb98d + + + + false + DEBUG;TRACE + + + true + TRACE + + + + + + + 3.5 + + + + + + + + Properties\CommonAssemblyInfo.cs + + + + + + + + {682e771a-76f7-4972-bbdc-1250b67f399b} + T4Toolbox + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/src/T4Toolbox.DirectiveProcessors/TransformationContextProcessor.cs b/src/T4Toolbox.DirectiveProcessors/TransformationContextProcessor.cs new file mode 100644 index 0000000..a433a26 --- /dev/null +++ b/src/T4Toolbox.DirectiveProcessors/TransformationContextProcessor.cs @@ -0,0 +1,78 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.DirectiveProcessors +{ + using System.CodeDom; + using System.Collections.Generic; + + /// + /// Generates initialization and cleanup logic required for . + /// + public class TransformationContextProcessor : DirectiveProcessor + { + /// + /// Name of the directive processor, as referenced by the templates. + /// + public const string Name = "T4Toolbox.TransformationContextProcessor"; + + private bool directiveProcessed = false; + + /// + /// Initializes a new instance of the class. + /// + public TransformationContextProcessor() + { + // Force T4 generate Host property, even if the template directive does not say so explicitly. + this.RequiresProcessingRunIsHostSpecific = true; + } + + /// + /// Gets the directive name as it is supposed to be used in template code. + /// + /// + /// A that contains name of the TransformationContext directive. + /// + protected override string DirectiveName + { + get { return "TransformationContext"; } + } + + /// + /// Generates code in the class area of the transformation class created by the T4 engine. + /// + /// The arguments for the directive. + protected override void Process(IDictionary arguments) + { + // Don't generate the same code more then once if T4Toolbox.tt happens to be included multiple times + if (this.directiveProcessed) + { + this.Warning("Multiple <#@ include file=\"T4Toolbox.tt\" #> directives were found in the template."); + return; + } + + this.References.Add(typeof(TransformationContext).Assembly.Location); + + // Add the following method call to the Initialize method of the generated text template. + // T4Toolbox.TransformationContext.Initialize(this, this.GenerationEnvironment); + var initialize = new CodeExpressionStatement( + new CodeMethodInvokeExpression( + new CodeTypeReferenceExpression(typeof(TransformationContext).FullName), + "Initialize", + new CodeThisReferenceExpression(), + new CodePropertyReferenceExpression(new CodeThisReferenceExpression(), "GenerationEnvironment"))); + this.LanguageProvider.GenerateCodeFromStatement(initialize, this.PreInitializationCode, null); + + // Add the following method call to the Dispose(bool) method of the generated text template. + // T4Toolbox.TransformationContext.Cleanup(); + var cleanup = new CodeExpressionStatement( + new CodeMethodInvokeExpression( + new CodeTypeReferenceExpression(typeof(TransformationContext).FullName), + "Cleanup")); + this.LanguageProvider.GenerateCodeFromStatement(cleanup, this.DisposeCode, null); + + this.directiveProcessed = true; + } + } +} diff --git a/src/T4Toolbox.DirectiveProcessors/packages.config b/src/T4Toolbox.DirectiveProcessors/packages.config new file mode 100644 index 0000000..a849236 --- /dev/null +++ b/src/T4Toolbox.DirectiveProcessors/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio.ItemTemplates/C#/Generator/Generator.tt b/src/T4Toolbox.VisualStudio.ItemTemplates/C#/Generator/Generator.tt new file mode 100644 index 0000000..43e9df6 --- /dev/null +++ b/src/T4Toolbox.VisualStudio.ItemTemplates/C#/Generator/Generator.tt @@ -0,0 +1,13 @@ +<#+ +// +// Copyright © $registeredorganization$. All Rights Reserved. +// + +public class $fileinputname$ : Generator +{ + protected override void RunCore() + { + + } +} +#> diff --git a/src/T4Toolbox.VisualStudio.ItemTemplates/C#/Generator/Generator.vstemplate b/src/T4Toolbox.VisualStudio.ItemTemplates/C#/Generator/Generator.vstemplate new file mode 100644 index 0000000..f77d46d --- /dev/null +++ b/src/T4Toolbox.VisualStudio.ItemTemplates/C#/Generator/Generator.vstemplate @@ -0,0 +1,28 @@ + + + + Generator.tt + Generator + An empty code generator + CSharp + + + + Generator.tt + + + + + + + Microsoft.VSDesigner, Version=11.0.0.0, Culture=Neutral, PublicKeyToken=b03f5f7f11d50a3a + Microsoft.VSDesigner.ProjectWizard.ItemPropertyWizard + + \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio.ItemTemplates/C#/Script/Script.tt b/src/T4Toolbox.VisualStudio.ItemTemplates/C#/Script/Script.tt new file mode 100644 index 0000000..7b0d7c8 --- /dev/null +++ b/src/T4Toolbox.VisualStudio.ItemTemplates/C#/Script/Script.tt @@ -0,0 +1,10 @@ +<#@ template language="C#" debug="True" #> +<#@ output extension="cs" #> +<#@ include file="T4Toolbox.tt" #> +<# +// +// Copyright © $registeredorganization$. All Rights Reserved. +// + + +#> diff --git a/src/T4Toolbox.VisualStudio.ItemTemplates/C#/Script/Script.vstemplate b/src/T4Toolbox.VisualStudio.ItemTemplates/C#/Script/Script.vstemplate new file mode 100644 index 0000000..5d7e12d --- /dev/null +++ b/src/T4Toolbox.VisualStudio.ItemTemplates/C#/Script/Script.vstemplate @@ -0,0 +1,25 @@ + + + + Script.tt + Script + A T4 text template that can generate multiple output files. + CSharp + + + + Script.tt + + + + + + + Microsoft.VSDesigner, Version=11.0.0.0, Culture=Neutral, PublicKeyToken=b03f5f7f11d50a3a + Microsoft.VSDesigner.ProjectWizard.ItemPropertyWizard + + \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio.ItemTemplates/C#/Template/Template.tt b/src/T4Toolbox.VisualStudio.ItemTemplates/C#/Template/Template.tt new file mode 100644 index 0000000..5809091 --- /dev/null +++ b/src/T4Toolbox.VisualStudio.ItemTemplates/C#/Template/Template.tt @@ -0,0 +1,25 @@ +<#+ +// +// Copyright © $registeredorganization$. All Rights Reserved. +// + +public class $fileinputname$ : CSharpTemplate +{ + public override string TransformText() + { + base.TransformText(); +#> +namespace <#= DefaultNamespace #> +{ + public class <#= Identifier("Sample Class") #> + { + private string <#= FieldName("Sample Field") #>; + + public string <#= PropertyName("Sample Property") #> { get; set; } + } +} +<#+ + return this.GenerationEnvironment.ToString(); + } +} +#> diff --git a/src/T4Toolbox.VisualStudio.ItemTemplates/C#/Template/Template.vstemplate b/src/T4Toolbox.VisualStudio.ItemTemplates/C#/Template/Template.vstemplate new file mode 100644 index 0000000..e7e9770 --- /dev/null +++ b/src/T4Toolbox.VisualStudio.ItemTemplates/C#/Template/Template.vstemplate @@ -0,0 +1,28 @@ + + + + Template.tt + Template + An empty code generation template + CSharp + + + + Template.tt + + + + + + + Microsoft.VSDesigner, Version=11.0.0.0, Culture=Neutral, PublicKeyToken=b03f5f7f11d50a3a + Microsoft.VSDesigner.ProjectWizard.ItemPropertyWizard + + \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio.ItemTemplates/T4Toolbox.VisualStudio.ItemTemplates.csproj b/src/T4Toolbox.VisualStudio.ItemTemplates/T4Toolbox.VisualStudio.ItemTemplates.csproj new file mode 100644 index 0000000..c9420a9 --- /dev/null +++ b/src/T4Toolbox.VisualStudio.ItemTemplates/T4Toolbox.VisualStudio.ItemTemplates.csproj @@ -0,0 +1,69 @@ + + + + 12.0 + 11.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + Debug + AnyCPU + {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + {EA04B345-97BE-4A49-9C9C-3EBD4F5D2250} + Library + Properties + T4Toolbox.VisualStudio.ItemTemplates + T4Toolbox.VisualStudio.ItemTemplates + + false + + false + false + false + false + false + false + false + false + false + false + + + + + + T4 Toolbox + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio.ItemTemplates/VB/Generator/Generator.tt b/src/T4Toolbox.VisualStudio.ItemTemplates/VB/Generator/Generator.tt new file mode 100644 index 0000000..e3e9c7a --- /dev/null +++ b/src/T4Toolbox.VisualStudio.ItemTemplates/VB/Generator/Generator.tt @@ -0,0 +1,14 @@ +<#+ +' +' Copyright © $registeredorganization$. All Rights Reserved. +' + +Public Class $fileinputname$ + Inherits Generator + + Protected Overrides Sub RunCore() + + End Sub + +End Class +#> diff --git a/src/T4Toolbox.VisualStudio.ItemTemplates/VB/Generator/Generator.vstemplate b/src/T4Toolbox.VisualStudio.ItemTemplates/VB/Generator/Generator.vstemplate new file mode 100644 index 0000000..8003592 --- /dev/null +++ b/src/T4Toolbox.VisualStudio.ItemTemplates/VB/Generator/Generator.vstemplate @@ -0,0 +1,28 @@ + + + + Generator.tt + Generator + An empty code generator + VisualBasic + + + + Generator.tt + + + + + + + Microsoft.VSDesigner, Version=11.0.0.0, Culture=Neutral, PublicKeyToken=b03f5f7f11d50a3a + Microsoft.VSDesigner.ProjectWizard.ItemPropertyWizard + + \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio.ItemTemplates/VB/Script/Script.tt b/src/T4Toolbox.VisualStudio.ItemTemplates/VB/Script/Script.tt new file mode 100644 index 0000000..667c3ce --- /dev/null +++ b/src/T4Toolbox.VisualStudio.ItemTemplates/VB/Script/Script.tt @@ -0,0 +1,10 @@ +<#@ template language="VB" debug="True" #> +<#@ output extension="vb" #> +<#@ include file="T4Toolbox.tt" #> +<# +' +' Copyright © $registeredorganization$. All Rights Reserved. +' + + +#> diff --git a/src/T4Toolbox.VisualStudio.ItemTemplates/VB/Script/Script.vstemplate b/src/T4Toolbox.VisualStudio.ItemTemplates/VB/Script/Script.vstemplate new file mode 100644 index 0000000..70203f6 --- /dev/null +++ b/src/T4Toolbox.VisualStudio.ItemTemplates/VB/Script/Script.vstemplate @@ -0,0 +1,25 @@ + + + + Script.tt + Script + A T4 text template that can generate multiple output files. + VisualBasic + + + + Script.tt + + + + + + + Microsoft.VSDesigner, Version=11.0.0.0, Culture=Neutral, PublicKeyToken=b03f5f7f11d50a3a + Microsoft.VSDesigner.ProjectWizard.ItemPropertyWizard + + \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio.ItemTemplates/VB/Template/Template.tt b/src/T4Toolbox.VisualStudio.ItemTemplates/VB/Template/Template.tt new file mode 100644 index 0000000..6bb8f25 --- /dev/null +++ b/src/T4Toolbox.VisualStudio.ItemTemplates/VB/Template/Template.tt @@ -0,0 +1,15 @@ +<#+ +' +' Copyright © $registeredorganization$. All Rights Reserved. +' + +Public Class $fileinputname$ + Inherits Template + + Public Overrides Function TransformText() As String + + Return Me.GenerationEnvironment.ToString() + End Function + +End Class +#> diff --git a/src/T4Toolbox.VisualStudio.ItemTemplates/VB/Template/Template.vstemplate b/src/T4Toolbox.VisualStudio.ItemTemplates/VB/Template/Template.vstemplate new file mode 100644 index 0000000..9389ac9 --- /dev/null +++ b/src/T4Toolbox.VisualStudio.ItemTemplates/VB/Template/Template.vstemplate @@ -0,0 +1,28 @@ + + + + Template.tt + Template + An empty code generation template + VisualBasic + + + + Template.tt + + + + + + + Microsoft.VSDesigner, Version=11.0.0.0, Culture=Neutral, PublicKeyToken=b03f5f7f11d50a3a + Microsoft.VSDesigner.ProjectWizard.ItemPropertyWizard + + \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/AssemblyInfo.cs b/src/T4Toolbox.VisualStudio/AssemblyInfo.cs new file mode 100644 index 0000000..07b08f6 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/AssemblyInfo.cs @@ -0,0 +1,44 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +using System; +using System.Reflection; +using Microsoft.VisualStudio.Shell; + +[assembly: AssemblyTitle(T4Toolbox.VisualStudio.AssemblyInfo.Name)] +[assembly: CLSCompliant(false)] + +// Help Visual Studio resolve references to the T4Toolbox assembly. +[assembly: ProvideCodeBase( + AssemblyName = T4Toolbox.AssemblyInfo.Name, + Version = T4Toolbox.AssemblyInfo.Version, + CodeBase = @"$PackageFolder$\" + T4Toolbox.AssemblyInfo.Name + ".dll")] + +// This is currently required because MPF enumerates attributes applied to T4ToolboxPackage class when instantiating +// the T4ToolboxOptionsPage. This can be eliminated if I can figure out how to register directive processors via MEF instead +// of ProvideDirectiveProcessorAttribute. +[assembly: ProvideCodeBase( + AssemblyName = T4Toolbox.DirectiveProcessors.AssemblyInfo.Name, + Version = T4Toolbox.DirectiveProcessors.AssemblyInfo.Version, + CodeBase = @"$PackageFolder$\" + T4Toolbox.DirectiveProcessors.AssemblyInfo.Name + ".dll")] + +// Help Visual Studio resolve references to the T4Toolbox.VisualStudio assembly. +[assembly: ProvideCodeBase( + AssemblyName = T4Toolbox.VisualStudio.AssemblyInfo.Name, + Version = T4Toolbox.VisualStudio.AssemblyInfo.Version, + CodeBase = @"$PackageFolder$\" + T4Toolbox.VisualStudio.AssemblyInfo.Name + ".dll")] + +namespace T4Toolbox.VisualStudio +{ + /// + /// Defines constants describing the T4Toolbox.VisualStudio assembly. + /// + internal abstract class AssemblyInfo : T4Toolbox.AssemblyInfo + { + /// + /// Gets the name of the assembly. + /// + public new const string Name = "T4Toolbox.VisualStudio"; + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/BrowseObjectExtender.cs b/src/T4Toolbox.VisualStudio/BrowseObjectExtender.cs new file mode 100644 index 0000000..9d2dafb --- /dev/null +++ b/src/T4Toolbox.VisualStudio/BrowseObjectExtender.cs @@ -0,0 +1,157 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio +{ + using System; + using System.ComponentModel; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Drawing.Design; + using System.Globalization; + using System.Runtime.InteropServices; + using EnvDTE; + using Microsoft.VisualStudio; + using Microsoft.VisualStudio.Shell.Interop; + + /// + /// Adds "Custom Tool Template" and "Custom ToolParameters" properties to the C# and Visual Basic file properties. + /// + [ComVisible(true), ClassInterface(ClassInterfaceType.AutoDispatch)] + [SuppressMessage("Microsoft.Interoperability", "CA1409:ComVisibleTypesShouldBeCreatable", Justification = "Instances of this type are created only by TemplatePropertyProvider.")] + public class BrowseObjectExtender + { + /// + /// Name used to register the extender with Visual Studio. + /// + /// + /// Visual Studio uses this name to determine name of the objects it creates. + /// Choosing this value allows us to create properties with clean names like T4Toolbox.CustomToolTemplate. + /// + internal const string Name = "T4Toolbox"; + + private const string TemplatePropertyDisplayName = "Custom Tool Template"; + + private readonly uint itemId; + private readonly IVsHierarchy hierarchy; + private readonly IVsBuildPropertyStorage propertyStorage; + private readonly IServiceProvider serviceProvider; + private readonly int cookie; + private readonly IExtenderSite site; + private ProjectItem projectItem; + + internal BrowseObjectExtender(IServiceProvider serviceProvider, IVsBrowseObject browseObject, IExtenderSite site, int cookie) + { + Debug.Assert(serviceProvider != null, "serviceProvider"); + Debug.Assert(browseObject != null, "browseObject"); + Debug.Assert(site != null, "site"); + Debug.Assert(cookie != 0, "cookie"); + + this.site = site; + this.cookie = cookie; + this.serviceProvider = serviceProvider; + ErrorHandler.ThrowOnFailure(browseObject.GetProjectItem(out this.hierarchy, out this.itemId)); + this.propertyStorage = (IVsBuildPropertyStorage)this.hierarchy; + this.CustomToolParameters = new CustomToolParameters(this.serviceProvider, this.hierarchy, this.itemId); + } + + /// + /// Finalizes an instance of the class. + /// + /// + /// This method notifies the extender site that the extender has been deleted in order to prevent Visual Studio + /// from crashing with an Access Violation exception. + /// + ~BrowseObjectExtender() + { + try + { + this.site.NotifyDelete(this.cookie); + } + catch (InvalidComObjectException) + { + // This exception occurs when the Runtime-Callable Wrapper (RCW) was already disconnected from the COM object. + // This typically happens when the extender is disposed when Visual Studio shuts down. + } + } + + /// + /// Gets the object that represents parameters defined in a text template. + /// + [DisplayName("Custom Tool Parameters"), Category("Advanced")] + [Description("Specifies values for parameters defined in a T4 template transformed by the TextTemplatingFileGenerator or the " + TemplatedFileGenerator.Name + ".")] + public CustomToolParameters CustomToolParameters { get; private set; } + + /// + /// Gets or sets the file name of the template used by the . + /// + [DisplayName(TemplatePropertyDisplayName), Category("Advanced")] + [Description("A T4 template used by the " + TemplatedFileGenerator.Name + " to generate code from this file.")] + [Editor(typeof(CustomToolTemplateEditor), typeof(UITypeEditor))] + public string CustomToolTemplate + { + get + { + string value; + if (ErrorHandler.Failed(this.propertyStorage.GetItemAttribute(this.itemId, ItemMetadata.Template, out value))) + { + // Metadata element is not defined. Return an empty string. + value = string.Empty; + } + + return value; + } + + set + { + if (!string.IsNullOrWhiteSpace(value)) + { + // Report an error if the user tries to specify template for an incompatible custom tool. + if (!string.IsNullOrWhiteSpace((string)this.ProjectItem.Properties.Item(ProjectItemProperty.CustomTool).Value) && + TemplatedFileGenerator.Name != (string)this.ProjectItem.Properties.Item(ProjectItemProperty.CustomTool).Value) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + "The '{0}' property is supported only by the {1}. Set the 'Custom Tool' property first.", + TemplatePropertyDisplayName, + TemplatedFileGenerator.Name)); + } + + // Report an error if the template cannot be found + string fullPath = value; + var templateLocator = (TemplateLocator)this.serviceProvider.GetService(typeof(TemplateLocator)); + if (!templateLocator.LocateTemplate(this.ProjectItem.FileNames[1], ref fullPath)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "Template '{0}' could not be found", value)); + } + } + + ErrorHandler.ThrowOnFailure(this.propertyStorage.SetItemAttribute(this.itemId, ItemMetadata.Template, value)); + + // If the file does not have a custom tool yet, assume that by specifying the template user wants to use the T4Toolbox.TemplatedFileGenerator. + if (!string.IsNullOrWhiteSpace(value) && + string.IsNullOrWhiteSpace((string)this.ProjectItem.Properties.Item(ProjectItemProperty.CustomTool).Value)) + { + this.ProjectItem.Properties.Item(ProjectItemProperty.CustomTool).Value = TemplatedFileGenerator.Name; + } + } + } + + private ProjectItem ProjectItem + { + get + { + if (this.projectItem == null) + { + object extObject; + ErrorHandler.ThrowOnFailure(this.hierarchy.GetProperty(this.itemId, (int)__VSHPROPID.VSHPROPID_ExtObject, out extObject)); + this.projectItem = (ProjectItem)extObject; + } + + return this.projectItem; + } + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/BrowseObjectExtenderProvider.cs b/src/T4Toolbox.VisualStudio/BrowseObjectExtenderProvider.cs new file mode 100644 index 0000000..659bb22 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/BrowseObjectExtenderProvider.cs @@ -0,0 +1,46 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio +{ + using System; + using EnvDTE; + using Microsoft.VisualStudio.Shell.Interop; + + /// + /// Provides a "Template" property extender for C# and Visual Basic project items. + /// + internal sealed class BrowseObjectExtenderProvider : IExtenderProvider, IDisposable + { + private const string ExtenderName = "T4Toolbox"; + + private readonly string extenderCategory; + private readonly ObjectExtenders objectExtenders; + private readonly int providerCookie; + private readonly IServiceProvider serviceProvider; + + public BrowseObjectExtenderProvider(IServiceProvider serviceProvider, string extenderCategory) + { + this.serviceProvider = serviceProvider; + this.objectExtenders = (ObjectExtenders)serviceProvider.GetService(typeof(ObjectExtenders)); + this.extenderCategory = extenderCategory; + this.providerCookie = this.objectExtenders.RegisterExtenderProvider(extenderCategory, BrowseObjectExtenderProvider.ExtenderName, this); + } + + public bool CanExtend(string extenderCategory, string extenderName, object extendee) + { + return extenderCategory == this.extenderCategory && extenderName == ExtenderName && extendee is IVsBrowseObject; + } + + public void Dispose() + { + this.objectExtenders.UnregisterExtenderProvider(this.providerCookie); + } + + public object GetExtender(string extenderCategory, string extenderName, object extendee, IExtenderSite site, int extenderCookie) + { + return new BrowseObjectExtender(this.serviceProvider, (IVsBrowseObject)extendee, site, extenderCookie); + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/CustomToolParameter.cs b/src/T4Toolbox.VisualStudio/CustomToolParameter.cs new file mode 100644 index 0000000..6fcea63 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/CustomToolParameter.cs @@ -0,0 +1,145 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio +{ + using System; + using System.ComponentModel; + using System.Diagnostics; + using System.Globalization; + + using Microsoft.VisualStudio; + using Microsoft.VisualStudio.Shell.Interop; + + /// + /// Represents a single parameter defined in a text template in the Properties window of Visual Studio. + /// + internal class CustomToolParameter : PropertyDescriptor + { + private readonly Type parameterType; + private readonly string description; + private readonly TypeConverter converter; + + public CustomToolParameter(string parameterName, Type parameterType, string description) + : base(parameterName, null) + { + Debug.Assert(!string.IsNullOrEmpty(parameterName), "parameterName"); + Debug.Assert(parameterType != null, "parameterType"); + Debug.Assert(description != null, "description"); + + this.parameterType = parameterType; + this.description = description; + this.converter = TypeDescriptor.GetConverter(parameterType); + } + + public override Type ComponentType + { + get { return typeof(CustomToolParameters); } + } + + public override bool IsReadOnly + { + get { return !this.converter.CanConvertTo(typeof(string)) || !this.converter.CanConvertFrom(typeof(string)); } + } + + public override Type PropertyType + { + get { return this.parameterType; } + } + + public override bool CanResetValue(object component) + { + return true; + } + + public override object GetValue(object component) + { + IVsBuildPropertyStorage project; + uint itemId; + GetProjectItem(component, out project, out itemId); + + string stringValue; + if (ErrorHandler.Failed(project.GetItemAttribute(itemId, this.Name, out stringValue))) + { + return this.GetDefaultValue(); + } + + return this.converter.ConvertFrom(stringValue); + } + + public override void ResetValue(object component) + { + IVsBuildPropertyStorage project; + uint itemId; + GetProjectItem(component, out project, out itemId); + + ErrorHandler.ThrowOnFailure(project.SetItemAttribute(itemId, this.Name, null)); + } + + public override void SetValue(object component, object value) + { + IVsBuildPropertyStorage project; + uint itemId; + GetProjectItem(component, out project, out itemId); + + if (object.Equals(value, this.GetDefaultValue())) + { + ErrorHandler.ThrowOnFailure(project.SetItemAttribute(itemId, this.Name, null)); + } + else + { + string stringValue = this.converter.ConvertToInvariantString(value); + ErrorHandler.ThrowOnFailure(project.SetItemAttribute(itemId, this.Name, stringValue)); + } + } + + /// + /// Returns true when property value is different than the default. + /// + /// + /// This is used by the PropertyGrid in Visual Studio to display values that are actually stored in bold font. + /// + public override bool ShouldSerializeValue(object component) + { + return !object.Equals(this.GetValue(component), this.GetDefaultValue()); + } + + protected override AttributeCollection CreateAttributeCollection() + { + if (!string.IsNullOrWhiteSpace(this.description)) + { + return AttributeCollection.FromExisting(base.CreateAttributeCollection(), new DescriptionAttribute(this.description)); + } + + return base.CreateAttributeCollection(); + } + + private static void GetProjectItem(object component, out IVsBuildPropertyStorage project, out uint itemId) + { + if (component == null) + { + throw new ArgumentNullException("component"); + } + + var parent = component as CustomToolParameters; + if (parent == null) + { + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + "Object of type {0} is expected, actual object is of type {1}.", + typeof(CustomToolParameters).FullName, + component.GetType().FullName), + "component"); + } + + parent.GetProjectItem(out project, out itemId); + } + + private object GetDefaultValue() + { + return this.parameterType.IsValueType && this.parameterType != typeof(void) ? Activator.CreateInstance(this.parameterType) : null; + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/CustomToolParameters.cs b/src/T4Toolbox.VisualStudio/CustomToolParameters.cs new file mode 100644 index 0000000..5b00a67 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/CustomToolParameters.cs @@ -0,0 +1,306 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Linq; + using System.Reflection; + using System.Text.RegularExpressions; + using Microsoft.VisualStudio; + using Microsoft.VisualStudio.Shell.Interop; + using Microsoft.VisualStudio.TextTemplating; + using Microsoft.VisualStudio.TextTemplating.VSHost; + + /// + /// Represents a collection of text template parameters in the Properties window of Visual Studio. + /// + /// + /// This class implements the interface to present template parameters + /// as a dynamic list of objects. + /// + [TypeConverter(typeof(ExpandableObjectConverter))] + public class CustomToolParameters : ICustomTypeDescriptor + { + private const string FileGroup = "F"; + private const string NameGroup = "N"; + private const string TypeGroup = "T"; + + private static readonly Regex IncludeExpression = new Regex( + "<\\#\\@\\s*include\\s+file\\s*=\\s*\"(?<" + FileGroup + ">[^\"]*)\"\\s*\\#>", + RegexOptions.IgnoreCase); + + private static readonly Regex ParameterExpression = new Regex( + "<\\#\\@\\s*parameter\\s+name\\s*=\\s*\"(?<" + NameGroup + ">[^\"]*)\"\\s+type\\s*=\\s*\"(?<" + TypeGroup + ">[^\"]*)\"\\s*\\#>", + RegexOptions.IgnoreCase); + + private readonly IServiceProvider serviceProvider; + private readonly IVsHierarchy project; + private readonly uint projectItemId; + + private ITextTemplatingEngineHost templatingHost; + private ITextTemplating templatingService; + private string[] assemblyReferences; + + internal CustomToolParameters(IServiceProvider serviceProvider, IVsHierarchy project, uint projectItemId) + { + Debug.Assert(serviceProvider != null, "serviceProvider"); + Debug.Assert(project != null, "project"); + Debug.Assert(projectItemId != 0, "projectItemId"); + + this.serviceProvider = serviceProvider; + this.project = project; + this.projectItemId = projectItemId; + } + + /// + /// Returns a default collection of attributes applied to this class. + /// + public AttributeCollection GetAttributes() + { + return TypeDescriptor.GetAttributes(this, noCustomTypeDesc: true); + } + + /// + /// Returns the default name of this class. + /// + public string GetClassName() + { + return TypeDescriptor.GetClassName(this, noCustomTypeDesc: true); + } + + /// + /// Returns the default name of this component. + /// + public string GetComponentName() + { + return TypeDescriptor.GetComponentName(this, noCustomTypeDesc: true); + } + + /// + /// Returns a specified in a applied to this class. + /// + public TypeConverter GetConverter() + { + return TypeDescriptor.GetConverter(this, noCustomTypeDesc: true); + } + + /// + /// Returns a default event defined in this class. + /// + public EventDescriptor GetDefaultEvent() + { + return TypeDescriptor.GetDefaultEvent(this, noCustomTypeDesc: true); + } + + /// + /// Returns a default property defined in this class. + /// + public PropertyDescriptor GetDefaultProperty() + { + return TypeDescriptor.GetDefaultProperty(this, noCustomTypeDesc: true); + } + + /// + /// Returns a default editor of this class. + /// + public object GetEditor(Type editorBaseType) + { + return TypeDescriptor.GetEditor(this, editorBaseType, noCustomTypeDesc: true); + } + + /// + /// Returns a default collection of events with the given attributes defined in this class. + /// + public EventDescriptorCollection GetEvents(Attribute[] attributes) + { + return TypeDescriptor.GetEvents(this, attributes, noCustomTypeDesc: true); + } + + /// + /// Returns a default collection of events defined in this class. + /// + public EventDescriptorCollection GetEvents() + { + return TypeDescriptor.GetEvents(this, noCustomTypeDesc: true); + } + + /// + /// Returns a collection of objects representing parameters defined in a text template. + /// + public PropertyDescriptorCollection GetProperties() + { + return this.GetProperties(null); + } + + /// + /// Returns a collection of objects representing parameters defined in a text template. + /// + public PropertyDescriptorCollection GetProperties(Attribute[] attributes) + { + this.templatingService = (ITextTemplating)this.serviceProvider.GetService(typeof(STextTemplating)); + this.templatingHost = (ITextTemplatingEngineHost)this.templatingService; + + string templateFileName; + if (this.ResolveTemplate(out templateFileName)) + { + string templateContent = File.ReadAllText(templateFileName, EncodingHelper.GetEncoding(templateFileName)); + + this.templatingService.PreprocessTemplate(templateFileName, templateContent, null, "TemporaryClass", "T4Toolbox", out this.assemblyReferences); + for (int i = 0; i < this.assemblyReferences.Length; i++) + { + this.assemblyReferences[i] = this.templatingHost.ResolveAssemblyReference(this.assemblyReferences[i]); + } + + var parameters = new List(); + this.ParseParameters(templateContent, parameters); + return new PropertyDescriptorCollection(parameters.Cast().ToArray()); + } + + return PropertyDescriptorCollection.Empty; + } + + /// + /// Returns an owner of the specified . + /// + public object GetPropertyOwner(PropertyDescriptor pd) + { + return this; + } + + /// + /// Returns an empty string to prevent the type name from being displayed in Visual Studio Properties window. + /// + public override string ToString() + { + return string.Empty; + } + + internal void GetProjectItem(out IVsBuildPropertyStorage project, out uint itemId) + { + project = (IVsBuildPropertyStorage)this.project; + itemId = this.projectItemId; + } + + private void ParseParameters(string templateContent, List parameters) + { + // Parse any <#@ include #> directives from the template + MatchCollection includeMatches = IncludeExpression.Matches(templateContent); + foreach (Match includeMatch in includeMatches) + { + string includedFile = includeMatch.Groups[FileGroup].Value; + string loadedContent, loadedFile; + if (this.templatingHost.LoadIncludeText(includedFile, out loadedContent, out loadedFile)) + { + this.ParseParameters(loadedContent, parameters); + } + } + + // Parse any <#@ parameter #> directives from the template + MatchCollection matches = ParameterExpression.Matches(templateContent); + foreach (Match parameterMatch in matches) + { + parameters.Add(this.CreateParameter(parameterMatch)); + } + } + + private bool ResolveTemplate(out string templateFileName) + { + templateFileName = string.Empty; + + string inputFileName; + ErrorHandler.ThrowOnFailure(this.project.GetCanonicalName(this.projectItemId, out inputFileName)); + + var propertyStorage = (IVsBuildPropertyStorage)this.project; + + string generator; + if (ErrorHandler.Failed(propertyStorage.GetItemAttribute(this.projectItemId, ItemMetadata.Generator, out generator))) + { + return false; + } + + if (string.Equals(generator, "TextTemplatingFileGenerator", StringComparison.OrdinalIgnoreCase)) + { + templateFileName = inputFileName; + } + else if (string.Equals(generator, ScriptFileGenerator.Name, StringComparison.OrdinalIgnoreCase)) + { + if (ErrorHandler.Failed(propertyStorage.GetItemAttribute(this.projectItemId, ItemMetadata.LastGenOutput, out templateFileName))) + { + return false; + } + + templateFileName = Path.Combine(Path.GetDirectoryName(inputFileName), templateFileName); + } + else if (string.Equals(generator, TemplatedFileGenerator.Name, StringComparison.OrdinalIgnoreCase)) + { + if (ErrorHandler.Failed(propertyStorage.GetItemAttribute(this.projectItemId, ItemMetadata.Template, out templateFileName))) + { + return false; + } + + var templateLocator = (TemplateLocator)this.serviceProvider.GetService(typeof(TemplateLocator)); + if (!templateLocator.LocateTemplate(inputFileName, ref templateFileName)) + { + return false; + } + } + + return File.Exists(templateFileName); + } + + private CustomToolParameter CreateParameter(Match match) + { + // Resolve parameter type + string typeName = match.Groups[TypeGroup].Value; + string description = string.Empty; + Type type = null; + try + { + type = Type.GetType(typeName, throwOnError: true, assemblyResolver: null, typeResolver: this.ResolveType); + } + catch (TypeLoadException e) + { + type = typeof(void); + description = e.Message; + } + + string name = match.Groups[NameGroup].Value; + return new CustomToolParameter(name, type, description); + } + + [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFrom", Justification = "That's how the T4 Engine loads assemblies.")] + private Type ResolveType(Assembly assembly, string typeName, bool ignoreCase) + { + // Try among assemblies already loaded in the current AppDomain + Assembly[] loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (Assembly loadedAssembly in loadedAssemblies) + { + Type type = loadedAssembly.GetType(typeName, false, ignoreCase); + if (type != null) + { + return type; + } + } + + // Try among assemblies referenced by the template + foreach (string assemblyFileName in this.assemblyReferences) + { + Assembly referencedAssembly = Assembly.LoadFrom(assemblyFileName); + Type type = referencedAssembly.GetType(typeName, false, ignoreCase); + if (type != null) + { + return type; + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/CustomToolTemplateEditor.cs b/src/T4Toolbox.VisualStudio/CustomToolTemplateEditor.cs new file mode 100644 index 0000000..dd81e29 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/CustomToolTemplateEditor.cs @@ -0,0 +1,94 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio +{ + using System; + using System.ComponentModel; + using System.Drawing.Design; + using System.IO; + using Microsoft.VisualStudio; + using Microsoft.VisualStudio.Shell.Interop; + using Microsoft.Win32; + + /// + /// A specialized file name editor for the template property. + /// + public class CustomToolTemplateEditor : UITypeEditor + { + /// + /// Defines the editor as a modal dialog. + /// + public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) + { + return UITypeEditorEditStyle.Modal; + } + + /// + /// Uses the Windows Open File dialog to allow user to choose the template file. + /// + public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) + { + string templateFullPath = GetFullTemplatePath(context, (string)value); + + var dialog = new OpenFileDialog(); + dialog.Title = "Select Custom Tool Template"; + dialog.FileName = Path.GetFileName(templateFullPath); + dialog.InitialDirectory = Path.GetDirectoryName(templateFullPath); + dialog.Filter = "Text Templates (*.tt)|*.tt|All Files (*.*)|*.*"; + + if (dialog.ShowDialog() == true) + { + return GetRelativeTemplatePath(context, dialog.FileName); + } + + return value; + } + + private static string GetFullTemplatePath(ITypeDescriptorContext context, string fileName) + { + string inputFullPath = GetFullInputPath(context); + + if (string.IsNullOrEmpty(fileName)) + { + return Path.GetDirectoryName(inputFullPath) + Path.DirectorySeparatorChar; + } + + string templateFullPath = fileName; + var templateLocator = (TemplateLocator)context.GetService(typeof(TemplateLocator)); + if (!templateLocator.LocateTemplate(inputFullPath, ref templateFullPath)) + { + return Path.Combine(Path.GetDirectoryName(inputFullPath), fileName); + } + + return templateFullPath; + } + + private static string GetRelativeTemplatePath(ITypeDescriptorContext context, string fullPath) + { + string inputPath = GetFullInputPath(context); + string relativePath = FileMethods.GetRelativePath(inputPath, fullPath); + if (relativePath.StartsWith("." + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) + { + // Remove leading .\ from the path + relativePath = relativePath.Substring(relativePath.IndexOf(Path.DirectorySeparatorChar) + 1); + } + + return relativePath; + } + + private static string GetFullInputPath(ITypeDescriptorContext context) + { + var browseObject = (IVsBrowseObject)context.Instance; + + IVsHierarchy hierarchy; + uint itemId; + ErrorHandler.ThrowOnFailure(browseObject.GetProjectItem(out hierarchy, out itemId)); + + string inputFileName; + ErrorHandler.ThrowOnFailure(hierarchy.GetCanonicalName(itemId, out inputFileName)); + return inputFileName; + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/Editor/ClassificationFormatDefinitions.cs b/src/T4Toolbox.VisualStudio/Editor/ClassificationFormatDefinitions.cs new file mode 100644 index 0000000..ddb2b09 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Editor/ClassificationFormatDefinitions.cs @@ -0,0 +1,82 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio.Editor +{ + using System.ComponentModel.Composition; + using System.Windows.Media; + using Microsoft.VisualStudio.Text.Classification; + using Microsoft.VisualStudio.Utilities; + + /// + /// Provides metadata information for registering text template format definitions with the Visual Studio editor. + /// + internal static class ClassificationFormatDefinitions + { + [Export(typeof(EditorFormatDefinition))] + [Name("Text Template Attribute Name")] + [ClassificationType(ClassificationTypeNames = ClassificationTypeName.AttributeName)] + [UserVisible(true)] + [Order(Before = Priority.Default)] + internal sealed class AttributeName : ClassificationFormatDefinition + { + internal AttributeName() + { + this.ForegroundColor = Colors.Red; + } + } + + [Export(typeof(EditorFormatDefinition))] + [Name("Text Template Attribute Value")] + [ClassificationType(ClassificationTypeNames = ClassificationTypeName.AttributeValue)] + [UserVisible(true)] + [Order(Before = Priority.Default)] + internal sealed class AttributeValue : ClassificationFormatDefinition + { + internal AttributeValue() + { + this.ForegroundColor = Colors.Blue; + } + } + + [Export(typeof(EditorFormatDefinition))] + [Name("Text Template Code Block")] + [ClassificationType(ClassificationTypeNames = ClassificationTypeName.CodeBlock)] + [UserVisible(true)] + [Order(Before = Priority.Default)] + internal sealed class CodeBlock : ClassificationFormatDefinition + { + internal CodeBlock() + { + this.BackgroundColor = Colors.Lavender; + } + } + + [Export(typeof(EditorFormatDefinition))] + [Name("Text Template Delimiter")] + [ClassificationType(ClassificationTypeNames = ClassificationTypeName.Delimiter)] + [UserVisible(true)] + [Order(Before = Priority.Default)] + internal sealed class Delimiter : ClassificationFormatDefinition + { + internal Delimiter() + { + this.BackgroundColor = Colors.Yellow; + } + } + + [Export(typeof(EditorFormatDefinition))] + [Name("Text Template Directive Name")] + [ClassificationType(ClassificationTypeNames = ClassificationTypeName.DirectiveName)] + [UserVisible(true)] + [Order(Before = Priority.Default)] + internal sealed class DirectiveName : ClassificationFormatDefinition + { + internal DirectiveName() + { + this.ForegroundColor = Colors.Maroon; + } + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/Editor/ClassificationTypeDefinitions.cs b/src/T4Toolbox.VisualStudio/Editor/ClassificationTypeDefinitions.cs new file mode 100644 index 0000000..4c289ab --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Editor/ClassificationTypeDefinitions.cs @@ -0,0 +1,31 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio.Editor +{ + using System.ComponentModel.Composition; + using Microsoft.VisualStudio.Text.Classification; + using Microsoft.VisualStudio.Utilities; + + /// + /// Provides metadata information for registering text template classification types with the Visual Studio editor. + /// + internal static class ClassificationTypeDefinitions + { + [Export, Name(ClassificationTypeName.AttributeName)] + internal static ClassificationTypeDefinition AttributeName { get; set; } + + [Export, Name(ClassificationTypeName.AttributeValue)] + internal static ClassificationTypeDefinition AttributeValue { get; set; } + + [Export, Name(ClassificationTypeName.CodeBlock)] + internal static ClassificationTypeDefinition CodeBlock { get; set; } + + [Export, Name(ClassificationTypeName.Delimiter)] + internal static ClassificationTypeDefinition Delimiter { get; set; } + + [Export, Name(ClassificationTypeName.DirectiveName)] + internal static ClassificationTypeDefinition DirectiveName { get; set; } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/Editor/ClassificationTypeName.cs b/src/T4Toolbox.VisualStudio/Editor/ClassificationTypeName.cs new file mode 100644 index 0000000..17e75ee --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Editor/ClassificationTypeName.cs @@ -0,0 +1,18 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio.Editor +{ + /// + /// Defines names of Text Template classification types. + /// + internal static class ClassificationTypeName + { + internal const string AttributeName = TemplateContentType.Name + ".AttributeName"; + internal const string AttributeValue = TemplateContentType.Name + ".AttributeValue"; + internal const string CodeBlock = TemplateContentType.Name + ".CodeBlock"; + internal const string Delimiter = TemplateContentType.Name + ".Delimiter"; + internal const string DirectiveName = TemplateContentType.Name + ".DirectiveName"; + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/Editor/TemplateClassificationTagger.cs b/src/T4Toolbox.VisualStudio/Editor/TemplateClassificationTagger.cs new file mode 100644 index 0000000..5bfd117 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Editor/TemplateClassificationTagger.cs @@ -0,0 +1,102 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio.Editor +{ + using System; + using System.Diagnostics; + using System.Globalization; + using System.Windows; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Classification; + using Microsoft.VisualStudio.Text.Tagging; + using T4Toolbox.VisualStudio.TemplateAnalysis; + + internal sealed class TemplateClassificationTagger : SimpleTagger + { + private readonly ITextBuffer buffer; + private readonly IClassificationType attributeNameClassification; + private readonly IClassificationType attributeValueClassification; + private readonly IClassificationType codeBlockClassificaiton; + private readonly IClassificationType delimiterClassification; + private readonly IClassificationType directiveNameClassificaiton; + + internal TemplateClassificationTagger(ITextBuffer buffer, IClassificationTypeRegistryService classificationTypeRegistry) + : base(buffer) + { + Debug.Assert(classificationTypeRegistry != null, "classificationTypeRegistry"); + + this.buffer = buffer; + WeakEventManager.AddHandler(this.buffer, "Changed", this.BufferChanged); + + this.attributeNameClassification = classificationTypeRegistry.GetClassificationType(ClassificationTypeName.AttributeName); + this.attributeValueClassification = classificationTypeRegistry.GetClassificationType(ClassificationTypeName.AttributeValue); + this.delimiterClassification = classificationTypeRegistry.GetClassificationType(ClassificationTypeName.Delimiter); + this.directiveNameClassificaiton = classificationTypeRegistry.GetClassificationType(ClassificationTypeName.DirectiveName); + this.codeBlockClassificaiton = classificationTypeRegistry.GetClassificationType(ClassificationTypeName.CodeBlock); + + this.UpdateTagSpans(); + } + + private void BufferChanged(object sender, TextContentChangedEventArgs e) + { + this.UpdateTagSpans(); + } + + private void CreateTagSpans(ITextSnapshot snapshot) + { + var scanner = new TemplateScanner(snapshot.GetText()); + while (scanner.yylex() != (int)SyntaxKind.EOF) + { + SyntaxNode token = scanner.yylval; + IClassificationType classificationType; + switch (token.Kind) + { + case SyntaxKind.BlockEnd: + case SyntaxKind.ClassBlockStart: + case SyntaxKind.DirectiveBlockStart: + case SyntaxKind.ExpressionBlockStart: + case SyntaxKind.StatementBlockStart: + classificationType = this.delimiterClassification; + break; + + case SyntaxKind.DirectiveName: + classificationType = this.directiveNameClassificaiton; + break; + + case SyntaxKind.AttributeName: + classificationType = this.attributeNameClassification; + break; + + case SyntaxKind.AttributeValue: + classificationType = this.attributeValueClassification; + break; + + case SyntaxKind.Code: + classificationType = this.codeBlockClassificaiton; + break; + + case SyntaxKind.Equals: + case SyntaxKind.DoubleQuote: + // Ignore + continue; + + default: + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Unexpected SyntaxKind value {0}.", token.Kind)); + } + + this.CreateTagSpan(snapshot.CreateTrackingSpan(token.Span, SpanTrackingMode.EdgeNegative), new ClassificationTag(classificationType)); + } + } + + private void UpdateTagSpans() + { + using (this.Update()) + { + this.RemoveTagSpans(trackingTagSpan => true); // remove all tag spans + this.CreateTagSpans(this.buffer.CurrentSnapshot); + } + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/Editor/TemplateClassificationTaggerProvider.cs b/src/T4Toolbox.VisualStudio/Editor/TemplateClassificationTaggerProvider.cs new file mode 100644 index 0000000..2d2d0c8 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Editor/TemplateClassificationTaggerProvider.cs @@ -0,0 +1,29 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio.Editor +{ + using System.ComponentModel.Composition; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Classification; + using Microsoft.VisualStudio.Text.Tagging; + using Microsoft.VisualStudio.Utilities; + + [Export(typeof(ITaggerProvider)), TagType(typeof(ClassificationTag)), ContentType(TemplateContentType.Name)] + internal sealed class TemplateClassificationTaggerProvider : ITaggerProvider + { + [Import] + internal IClassificationTypeRegistryService ClassificationRegistry { private get; set; } + + public ITagger CreateTagger(ITextBuffer buffer) where T : ITag + { + if (T4ToolboxOptions.Instance.SyntaxColorizationEnabled) + { + return buffer.Properties.GetOrCreateSingletonProperty(() => new TemplateClassificationTagger(buffer, this.ClassificationRegistry)) as ITagger; + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/Editor/TemplateCompletionBuilder.cs b/src/T4Toolbox.VisualStudio/Editor/TemplateCompletionBuilder.cs new file mode 100644 index 0000000..2f4a796 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Editor/TemplateCompletionBuilder.cs @@ -0,0 +1,129 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio.Editor +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using Microsoft.VisualStudio.Language.Intellisense; + using T4Toolbox.VisualStudio.TemplateAnalysis; + using Attribute = T4Toolbox.VisualStudio.TemplateAnalysis.Attribute; + + /// + /// Worker for calculating a set of applicable to a syntax + /// at the specified position. + /// + internal sealed class TemplateCompletionBuilder : SyntaxNodeVisitor + { + private readonly int position; + private Attribute currentAttribute; + private Directive currentDirective; + + public TemplateCompletionBuilder(int position) + { + Debug.Assert(position >= 0, "position"); + this.position = position; + } + + /// + /// Gets the list of objects applicable to the . + /// Returns null if position was not visited or if node does not support completions. + /// + public IReadOnlyList Completions { get; private set; } + + /// + /// Gets the at position specified in the constructor. + /// Returns null if position was not visited. + /// + public SyntaxNode Node { get; private set; } + + protected internal override void VisitAttribute(Attribute node) + { + this.currentAttribute = node; + base.VisitAttribute(node); + } + + protected internal override void VisitAttributeName(AttributeName node) + { + base.VisitAttributeName(node); + + if (node.Span.Start <= this.position && this.position <= node.Span.End) + { + Debug.Assert(this.currentDirective != null, "currentDirective"); + var directiveDescriptor = DirectiveDescriptor.GetDirectiveDescriptor(this.currentDirective.GetType()); + + var completions = new List(); + foreach (AttributeDescriptor attribute in directiveDescriptor.Attributes.Values) + { + if (!this.currentDirective.Attributes.ContainsKey(attribute.DisplayName)) + { + completions.Add(CreateAttributeCompletion(attribute)); + } + } + + if (completions.Count > 0) + { + this.Completions = completions; + this.Node = node; + } + } + } + + protected internal override void VisitAttributeValue(AttributeValue node) + { + base.VisitAttributeValue(node); + + if (node.Span.Start <= this.position && this.position <= node.Span.End) + { + Debug.Assert(this.currentDirective != null, "currentDirective"); + Debug.Assert(this.currentAttribute != null, "currentAttribute"); + DirectiveDescriptor directiveDescriptor = DirectiveDescriptor.GetDirectiveDescriptor(this.currentDirective.GetType()); + AttributeDescriptor attributeDescriptor; + if (directiveDescriptor.Attributes.TryGetValue(this.currentAttribute.Name, out attributeDescriptor)) + { + this.Completions = new List(attributeDescriptor.Values.Values.Select(CreateAttributeValueCompletion)); + this.Node = node; + } + } + } + + protected internal override void VisitDirective(Directive node) + { + this.currentDirective = node; + base.VisitDirective(node); + } + + protected internal override void VisitDirectiveName(DirectiveName node) + { + base.VisitDirectiveName(node); + + if (node.Span.Start <= this.position && this.position <= node.Span.End) + { + this.Completions = DirectiveDescriptor.GetBuiltInDirectives() + .Where(descriptor => !string.IsNullOrEmpty(descriptor.DisplayName)) // Skip custom directives + .Select(CreateDirectiveCompletion) + .ToList(); + this.Node = node; + } + } + + private static Completion CreateAttributeCompletion(AttributeDescriptor attribute) + { + return new Completion(attribute.DisplayName, attribute.DisplayName, attribute.Description, null, null); + } + + private static Completion CreateAttributeValueCompletion(ValueDescriptor value) + { + Debug.Assert(value != null, "value"); + return new Completion(value.DisplayName, value.DisplayName, value.Description, null, null); + } + + private static Completion CreateDirectiveCompletion(DirectiveDescriptor directive) + { + return new Completion(directive.DisplayName, directive.DisplayName, directive.Description, null, null); + } + } +} diff --git a/src/T4Toolbox.VisualStudio/Editor/TemplateCompletionHandler.cs b/src/T4Toolbox.VisualStudio/Editor/TemplateCompletionHandler.cs new file mode 100644 index 0000000..6be72ff --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Editor/TemplateCompletionHandler.cs @@ -0,0 +1,106 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio.Editor +{ + using System; + using System.Diagnostics; + using System.Runtime.InteropServices; + using Microsoft.VisualStudio; + using Microsoft.VisualStudio.Language.Intellisense; + using Microsoft.VisualStudio.OLE.Interop; + using Microsoft.VisualStudio.Shell; + using Microsoft.VisualStudio.Text.Editor; + using IServiceProvider = System.IServiceProvider; + + /// + /// Initiates completion sessions for Text Templates in Visual Studio editors when user is typing directives. + /// + internal sealed class TemplateCompletionHandler : IOleCommandTarget + { + internal ICompletionBroker CompletionBroker; + internal IOleCommandTarget NextHandler; + internal IServiceProvider ServiceProvider; + internal ITextView TextView; + + private ICompletionSession completionSession; + + public int Exec(ref Guid commandGroup, uint command, uint options, IntPtr input, IntPtr output) + { + Debug.Assert(this.CompletionBroker != null, "completionBroker"); + Debug.Assert(this.NextHandler != null, "nextHandler"); + Debug.Assert(this.ServiceProvider != null, "serviceProvider"); + Debug.Assert(this.TextView != null, "textView"); + + if (VsShellUtilities.IsInAutomationFunction(this.ServiceProvider)) + { + return this.NextHandler.Exec(ref commandGroup, command, options, input, output); + } + + // Commit or dismiss the current completion session + if (this.completionSession != null && !this.completionSession.IsDismissed) + { + if (commandGroup == VSConstants.VSStd2K && + (command == (uint)VSConstants.VSStd2KCmdID.RETURN || command == (uint)VSConstants.VSStd2KCmdID.TAB)) + { + if (this.completionSession.SelectedCompletionSet.SelectionStatus.IsSelected) + { + this.completionSession.Commit(); + return VSConstants.S_OK; + } + + this.completionSession.Dismiss(); + } + } + + // Execute next handler to pass the command to the text buffer + int result = this.NextHandler.Exec(ref commandGroup, command, options, input, output); + + // Trigger new or filter the current completion session + if (commandGroup == VSConstants.VSStd2K) + { + if (command == (uint)VSConstants.VSStd2KCmdID.TYPECHAR && char.IsLetter((char)(ushort)Marshal.GetObjectForNativeVariant(input))) + { + if (this.completionSession == null) + { + this.completionSession = this.CompletionBroker.TriggerCompletion(this.TextView); + + // completion session may not have been created, perhaps because there are no completion sets at current caret position? + if (this.completionSession != null) + { + this.completionSession.Dismissed += this.CompletionSessionDismissed; + } + } + + if (this.completionSession != null && !this.completionSession.IsDismissed) + { + this.completionSession.Filter(); + } + } + + if (command == (uint)VSConstants.VSStd2KCmdID.BACKSPACE || command == (uint)VSConstants.VSStd2KCmdID.DELETE) + { + if (this.completionSession != null && !this.completionSession.IsDismissed) + { + this.completionSession.Filter(); + } + } + } + + return result; + } + + public int QueryStatus(ref Guid commandGroup, uint numberOfCommands, OLECMD[] commands, IntPtr commandText) + { + Debug.Assert(this.NextHandler != null, "nextHandler"); + return this.NextHandler.QueryStatus(ref commandGroup, numberOfCommands, commands, commandText); + } + + private void CompletionSessionDismissed(object sender, EventArgs e) + { + this.completionSession.Dismissed -= this.CompletionSessionDismissed; + this.completionSession = null; + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/Editor/TemplateCompletionHandlerProvider.cs b/src/T4Toolbox.VisualStudio/Editor/TemplateCompletionHandlerProvider.cs new file mode 100644 index 0000000..f9be7ce --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Editor/TemplateCompletionHandlerProvider.cs @@ -0,0 +1,56 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio.Editor +{ + using System.ComponentModel.Composition; + using System.Diagnostics; + using Microsoft.VisualStudio; + using Microsoft.VisualStudio.Editor; + using Microsoft.VisualStudio.Language.Intellisense; + using Microsoft.VisualStudio.Shell; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.TextManager.Interop; + using Microsoft.VisualStudio.Utilities; + + /// + /// Creates a for text template files opened in Visual Studio editor. + /// + [Export(typeof(IVsTextViewCreationListener)), ContentType(TemplateContentType.Name), TextViewRole(PredefinedTextViewRoles.Editable)] + internal sealed class TemplateCompletionHandlerProvider : IVsTextViewCreationListener + { + [Import]internal IVsEditorAdaptersFactoryService AdapterFactory; + [Import]internal SVsServiceProvider ServiceProvider; + [Import]internal ICompletionBroker CompletionBroker; + + public void VsTextViewCreated(IVsTextView viewAdapter) + { + Debug.Assert(this.AdapterFactory != null, "AdapterFactory"); + Debug.Assert(viewAdapter != null, "viewAdapter"); + + if (!T4ToolboxOptions.Instance.CompletionListsEnabled) + { + return; + } + + IWpfTextView textView = this.AdapterFactory.GetWpfTextView(viewAdapter); + if (textView == null) + { + return; + } + + textView.Properties.GetOrCreateSingletonProperty(() => this.CreateHandler(viewAdapter, textView)); + } + + private TemplateCompletionHandler CreateHandler(IVsTextView viewAdapter, IWpfTextView textView) + { + var handler = new TemplateCompletionHandler(); + handler.TextView = textView; + handler.ServiceProvider = this.ServiceProvider; + handler.CompletionBroker = this.CompletionBroker; + ErrorHandler.ThrowOnFailure(viewAdapter.AddCommandFilter(handler, out handler.NextHandler)); + return handler; + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/Editor/TemplateCompletionSource.cs b/src/T4Toolbox.VisualStudio/Editor/TemplateCompletionSource.cs new file mode 100644 index 0000000..24ec1f8 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Editor/TemplateCompletionSource.cs @@ -0,0 +1,52 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio.Editor +{ + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using Microsoft.VisualStudio.Language.Intellisense; + using Microsoft.VisualStudio.Text; + using T4Toolbox.VisualStudio.TemplateAnalysis; + + internal sealed class TemplateCompletionSource : ICompletionSource + { + private readonly ITextBuffer buffer; + private readonly TemplateAnalyzer analyzer; + + internal TemplateCompletionSource(ITextBuffer buffer) + { + Debug.Assert(buffer != null, "buffer"); + + this.buffer = buffer; + this.analyzer = TemplateAnalyzer.GetOrCreate(buffer); + } + + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "This method is called by the Visual Studio editor and expects to receive valid arguments")] + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1", Justification = "This method is called by the Visual Studio editor and expects to receive valid arguments")] + public void AugmentCompletionSession(ICompletionSession session, IList completionSets) + { + Debug.Assert(session != null, "session"); + Debug.Assert(completionSets != null, "completionSets"); + + TemplateAnalysis current = this.analyzer.CurrentAnalysis; + var builder = new TemplateCompletionBuilder(session.GetTriggerPoint(current.TextSnapshot).Value.Position); + builder.Visit(current.Template); + if (builder.Completions != null) + { + ITrackingSpan applicableTo = current.TextSnapshot.CreateTrackingSpan(builder.Node.Span, SpanTrackingMode.EdgeInclusive); + IEnumerable completions = builder.Completions.OrderBy(completion => completion.DisplayText); + var completionSet = new CompletionSet("All", "All", applicableTo, completions, null); + completionSets.Add(completionSet); + } + } + + public void Dispose() + { + this.buffer.Properties.RemoveProperty(this.GetType()); + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/Editor/TemplateCompletionSourceProvider.cs b/src/T4Toolbox.VisualStudio/Editor/TemplateCompletionSourceProvider.cs new file mode 100644 index 0000000..82a5914 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Editor/TemplateCompletionSourceProvider.cs @@ -0,0 +1,24 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio.Editor +{ + using System.ComponentModel.Composition; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using Microsoft.VisualStudio.Language.Intellisense; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Utilities; + + [Export(typeof(ICompletionSourceProvider)), Name(TemplateContentType.Name), ContentType(TemplateContentType.Name)] + internal sealed class TemplateCompletionSourceProvider : ICompletionSourceProvider + { + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "This method is called by only by the Visual Studio editor and assumes that a textBuffer is supplied")] + public ICompletionSource TryCreateCompletionSource(ITextBuffer textBuffer) + { + Debug.Assert(textBuffer != null, "textBuffer"); + return textBuffer.Properties.GetOrCreateSingletonProperty(() => new TemplateCompletionSource(textBuffer)); + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/Editor/TemplateContentType.cs b/src/T4Toolbox.VisualStudio/Editor/TemplateContentType.cs new file mode 100644 index 0000000..65ef703 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Editor/TemplateContentType.cs @@ -0,0 +1,20 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio.Editor +{ + using System.ComponentModel.Composition; + using Microsoft.VisualStudio.Utilities; + + internal static class TemplateContentType + { + internal const string Name = "TextTemplate"; + + [Export, Name(Name), BaseDefinition("code")] + internal static ContentTypeDefinition Definition { get; set; } // Used for metadata only + + [Export, FileExtension(".tt"), ContentType(Name)] + internal static FileExtensionToContentTypeDefinition FileAssociation { get; set; } // Used for metadata only + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/Editor/TemplateErrorReporter.cs b/src/T4Toolbox.VisualStudio/Editor/TemplateErrorReporter.cs new file mode 100644 index 0000000..33d39b8 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Editor/TemplateErrorReporter.cs @@ -0,0 +1,119 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio.Editor +{ + using System; + using System.Diagnostics; + using System.Windows; + using Microsoft.VisualStudio; + using Microsoft.VisualStudio.Shell; + using Microsoft.VisualStudio.Shell.Interop; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.TextManager.Interop; + using T4Toolbox.VisualStudio.TemplateAnalysis; + + /// + /// Displays template errors in the Error List window. + /// + internal sealed class TemplateErrorReporter : IDisposable + { + private readonly TemplateAnalyzer analyzer; + private readonly ITextDocument document; + private readonly IServiceProvider serviceProvider; + + private ErrorListProvider errorListProvider; + + private TemplateErrorReporter(ITextBuffer buffer, IServiceProvider serviceProvider, ITextDocumentFactoryService documentFactory) + { + Debug.Assert(buffer != null, "buffer"); + Debug.Assert(serviceProvider != null, "serviceProvider"); + Debug.Assert(documentFactory != null, "documentFactory"); + + this.serviceProvider = serviceProvider; + + documentFactory.TryGetTextDocument(buffer, out this.document); + WeakEventManager.AddHandler(documentFactory, "TextDocumentDisposed", this.DocumentDisposed); + + this.analyzer = TemplateAnalyzer.GetOrCreate(buffer); + WeakEventManager.AddHandler(this.analyzer, "TemplateChanged", this.TemplateChanged); + + this.UpdateErrorTasks(this.analyzer.CurrentAnalysis); + } + + public static TemplateErrorReporter GetOrCreate(ITextBuffer buffer, IServiceProvider serviceProvider, ITextDocumentFactoryService documentFactory) + { + return buffer.Properties.GetOrCreateSingletonProperty(() => new TemplateErrorReporter(buffer, serviceProvider, documentFactory)); + } + + public void Dispose() + { + this.document.TextBuffer.Properties.RemoveProperty(typeof(TemplateErrorReporter)); + + if (this.errorListProvider != null) + { + this.errorListProvider.Dispose(); + this.errorListProvider = null; + } + } + + private void DocumentDisposed(object sender, TextDocumentEventArgs e) + { + if (e.TextDocument == this.document) + { + this.Dispose(); + } + } + + private void TemplateChanged(object sender, TemplateAnalysis e) + { + this.UpdateErrorTasks(e); + } + + private void UpdateErrorTasks(TemplateAnalysis templateAnalysis) + { + if (this.errorListProvider != null) + { + this.errorListProvider.Tasks.Clear(); + } + else if (templateAnalysis.Errors.Count > 0) + { + this.errorListProvider = new ErrorListProvider(this.serviceProvider); + } + + foreach (TemplateError error in templateAnalysis.Errors) + { + var errorTask = new ErrorTask(); + errorTask.Document = this.document.FilePath; + errorTask.Category = TaskCategory.BuildCompile; + errorTask.Text = error.Message; + errorTask.ErrorCategory = TaskErrorCategory.Error; + errorTask.Line = error.Position.Line; + errorTask.Column = error.Position.Column; + errorTask.Navigate += this.NavigateToError; + this.errorListProvider.Tasks.Add(errorTask); + } + } + + private void NavigateToError(object sender, EventArgs e) + { + var errorTask = (ErrorTask)sender; + + IVsUIHierarchy hierarchyItem; + uint num; + IVsWindowFrame windowFrame; + VsShellUtilities.OpenDocument(this.serviceProvider, errorTask.Document, Guid.Empty, out hierarchyItem, out num, out windowFrame); + if (windowFrame != null) + { + errorTask.HierarchyItem = hierarchyItem; + this.errorListProvider.Refresh(); + IVsTextView textView = VsShellUtilities.GetTextView(windowFrame); + if (textView != null) + { + ErrorHandler.ThrowOnFailure(textView.SetSelection(errorTask.Line, errorTask.Column, errorTask.Line, errorTask.Column)); + } + } + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/Editor/TemplateErrorReporterProvider.cs b/src/T4Toolbox.VisualStudio/Editor/TemplateErrorReporterProvider.cs new file mode 100644 index 0000000..9d20ed2 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Editor/TemplateErrorReporterProvider.cs @@ -0,0 +1,32 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio.Editor +{ + using System.ComponentModel.Composition; + using Microsoft.VisualStudio.Shell; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Tagging; + using Microsoft.VisualStudio.Utilities; + + [Export(typeof(ITaggerProvider)), TagType(typeof(ErrorTag)), ContentType(TemplateContentType.Name)] + internal sealed class TemplateErrorReporterProvider : ITaggerProvider + { + [Import] + private SVsServiceProvider serviceProvider = null; + + [Import] + private ITextDocumentFactoryService documentFactory = null; + + public ITagger CreateTagger(ITextBuffer buffer) where T : ITag + { + if (T4ToolboxOptions.Instance.ErrorReportingEnabled) + { + TemplateErrorReporter.GetOrCreate(buffer, this.serviceProvider, this.documentFactory); + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/Editor/TemplateErrorTagger.cs b/src/T4Toolbox.VisualStudio/Editor/TemplateErrorTagger.cs new file mode 100644 index 0000000..3fcb85e --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Editor/TemplateErrorTagger.cs @@ -0,0 +1,30 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio.Editor +{ + using System.Diagnostics.CodeAnalysis; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Adornments; + using Microsoft.VisualStudio.Text.Tagging; + using T4Toolbox.VisualStudio.TemplateAnalysis; + + internal sealed class TemplateErrorTagger : TemplateTagger + { + public TemplateErrorTagger(ITextBuffer buffer) : base(buffer) + { + this.UpdateTagSpans(this.Analyzer.CurrentAnalysis); + } + + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "This is an internal method, called by base class.")] + protected override void CreateTagSpans(TemplateAnalysis analysis) + { + ITextSnapshot snapshot = analysis.TextSnapshot; + foreach (TemplateError error in analysis.Errors) + { + this.CreateTagSpan(snapshot.CreateTrackingSpan(error.Span, SpanTrackingMode.EdgeNegative), new ErrorTag(PredefinedErrorTypeNames.SyntaxError, error.Message)); + } + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/Editor/TemplateErrorTaggerProvider.cs b/src/T4Toolbox.VisualStudio/Editor/TemplateErrorTaggerProvider.cs new file mode 100644 index 0000000..3351472 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Editor/TemplateErrorTaggerProvider.cs @@ -0,0 +1,31 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio.Editor +{ + using System; + using System.ComponentModel.Composition; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Tagging; + using Microsoft.VisualStudio.Utilities; + + [Export(typeof(ITaggerProvider)), TagType(typeof(ErrorTag)), ContentType(TemplateContentType.Name)] + internal sealed class TemplateErrorTaggerProvider : ITaggerProvider + { + public ITagger CreateTagger(ITextBuffer buffer) where T : ITag + { + if (buffer == null) + { + throw new ArgumentNullException("buffer"); + } + + if (T4ToolboxOptions.Instance.ErrorUnderliningEnabled) + { + return buffer.Properties.GetOrCreateSingletonProperty(() => new TemplateErrorTagger(buffer)) as ITagger; + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/Editor/TemplateOutliningTagger.cs b/src/T4Toolbox.VisualStudio/Editor/TemplateOutliningTagger.cs new file mode 100644 index 0000000..441d3fd --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Editor/TemplateOutliningTagger.cs @@ -0,0 +1,83 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio.Editor +{ + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Linq; + using System.Text; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Tagging; + using T4Toolbox.VisualStudio.TemplateAnalysis; + + internal sealed class TemplateOutliningTagger : TemplateTagger + { + public TemplateOutliningTagger(ITextBuffer buffer) : base(buffer) + { + this.UpdateTagSpans(this.Analyzer.CurrentAnalysis); + } + + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "This is an internal method, called by base class.")] + protected override void CreateTagSpans(TemplateAnalysis analysis) + { + // If text buffer contains recognizable template + Template template = analysis.Template; + if (template != null) + { + ITextSnapshot snapshot = analysis.TextSnapshot; + string text = snapshot.GetText(); + foreach (CodeBlock codeBlock in template.ChildNodes().OfType()) + { + ITrackingSpan trackingSpan = snapshot.CreateTrackingSpan(codeBlock.Span, SpanTrackingMode.EdgeNegative); + + string collapsedForm = GetCollapsedForm(codeBlock, text); + string collapsedHintForm = GetCollapsedHintForm(codeBlock, text); + var tag = new OutliningRegionTag(collapsedForm, collapsedHintForm); + + this.CreateTagSpan(trackingSpan, tag); + } + } + } + + private static string GetCollapsedForm(CodeBlock codeBlock, string template) + { + var text = new StringBuilder(); + text.Append(codeBlock.Start.GetText(template)); + text.Append("..."); + text.Append(codeBlock.End.GetText(template)); + return text.ToString(); + } + + private static string GetCollapsedHintForm(CodeBlock codeBlock, string template) + { + var text = new StringBuilder(); + + using (var reader = new StringReader(codeBlock.GetText(template))) + { + for (int i = 0; i < 10; i++) + { + string line = reader.ReadLine(); + if (line == null) + { + return text.ToString(); + } + + // Append new line manually to avoid unnecessary \r\n at the end + if (i > 0) + { + text.AppendLine(); + } + + text.Append(line); + } + + text.AppendLine(); + text.Append("..."); + } + + return text.ToString(); + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/Editor/TemplateOutliningTaggerProvider.cs b/src/T4Toolbox.VisualStudio/Editor/TemplateOutliningTaggerProvider.cs new file mode 100644 index 0000000..4767187 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Editor/TemplateOutliningTaggerProvider.cs @@ -0,0 +1,31 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio.Editor +{ + using System; + using System.ComponentModel.Composition; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Tagging; + using Microsoft.VisualStudio.Utilities; + + [Export(typeof(ITaggerProvider)), TagType(typeof(OutliningRegionTag)), ContentType(TemplateContentType.Name)] + internal sealed class TemplateOutliningTaggerProvider : ITaggerProvider + { + public ITagger CreateTagger(ITextBuffer buffer) where T : ITag + { + if (buffer == null) + { + throw new ArgumentNullException("buffer"); + } + + if (T4ToolboxOptions.Instance.TemplateOutliningEnabled) + { + return buffer.Properties.GetOrCreateSingletonProperty(() => new TemplateOutliningTagger(buffer)) as ITagger; + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/Editor/TemplateQuickInfoSource.cs b/src/T4Toolbox.VisualStudio/Editor/TemplateQuickInfoSource.cs new file mode 100644 index 0000000..6190c62 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Editor/TemplateQuickInfoSource.cs @@ -0,0 +1,58 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio.Editor +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using Microsoft.VisualStudio.Language.Intellisense; + using Microsoft.VisualStudio.Text; + using T4Toolbox.VisualStudio.TemplateAnalysis; + + internal sealed class TemplateQuickInfoSource : IQuickInfoSource + { + private readonly TemplateAnalyzer analyzer; + + public TemplateQuickInfoSource(ITextBuffer buffer) + { + Debug.Assert(buffer != null, "buffer"); + this.analyzer = TemplateAnalyzer.GetOrCreate(buffer); + } + + public void AugmentQuickInfoSession(IQuickInfoSession session, IList quickInfoContent, out ITrackingSpan applicableToSpan) + { + if (session == null) + { + throw new ArgumentNullException("session"); + } + + if (quickInfoContent == null) + { + throw new ArgumentNullException("quickInfoContent"); + } + + TemplateAnalysis analysis = this.analyzer.CurrentAnalysis; + SnapshotPoint? triggerPoint = session.GetTriggerPoint(analysis.TextSnapshot); + if (triggerPoint != null && analysis.Template != null) + { + string description; + Span applicableTo; + if (analysis.Template.TryGetDescription(triggerPoint.Value.Position, out description, out applicableTo)) + { + quickInfoContent.Add(description); + applicableToSpan = analysis.TextSnapshot.CreateTrackingSpan(applicableTo, SpanTrackingMode.EdgeExclusive); + return; + } + } + + applicableToSpan = null; + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/Editor/TemplateQuickInfoSourceProvider.cs b/src/T4Toolbox.VisualStudio/Editor/TemplateQuickInfoSourceProvider.cs new file mode 100644 index 0000000..1bfd702 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Editor/TemplateQuickInfoSourceProvider.cs @@ -0,0 +1,32 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio.Editor +{ + using System; + using System.ComponentModel.Composition; + using Microsoft.VisualStudio.Language.Intellisense; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Utilities; + + [Export(typeof(IQuickInfoSourceProvider)), ContentType(TemplateContentType.Name)] + [Name("Template Quick Info Source"), Order(Before = "Default Quick Info Presenter")] + internal sealed class TemplateQuickInfoSourceProvider : IQuickInfoSourceProvider + { + public IQuickInfoSource TryCreateQuickInfoSource(ITextBuffer buffer) + { + if (buffer == null) + { + throw new ArgumentNullException("buffer"); + } + + if (T4ToolboxOptions.Instance.QuickInfoTooltipsEnabled) + { + return buffer.Properties.GetOrCreateSingletonProperty(() => new TemplateQuickInfoSource(buffer)); + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/Editor/TemplateTagger.cs b/src/T4Toolbox.VisualStudio/Editor/TemplateTagger.cs new file mode 100644 index 0000000..424ea9e --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Editor/TemplateTagger.cs @@ -0,0 +1,43 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio.Editor +{ + using System.Windows; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Tagging; + using T4Toolbox.VisualStudio.TemplateAnalysis; + + internal abstract class TemplateTagger : SimpleTagger where T : ITag + { + private readonly TemplateAnalyzer analyzer; + + protected TemplateTagger(ITextBuffer buffer) : base(buffer) + { + this.analyzer = TemplateAnalyzer.GetOrCreate(buffer); + WeakEventManager.AddHandler(this.analyzer, "TemplateChanged", this.TemplateChanged); + } + + protected TemplateAnalyzer Analyzer + { + get { return this.analyzer; } + } + + protected abstract void CreateTagSpans(TemplateAnalysis analysis); + + protected void UpdateTagSpans(TemplateAnalysis templateAnalysis) + { + using (this.Update()) + { + this.RemoveTagSpans(trackingTagSpan => true); // remove all tag spans + this.CreateTagSpans(templateAnalysis); + } + } + + private void TemplateChanged(object sender, TemplateAnalysis currentAnalysis) + { + this.UpdateTagSpans(currentAnalysis); + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/EnvDteExtensions.cs b/src/T4Toolbox.VisualStudio/EnvDteExtensions.cs new file mode 100644 index 0000000..4711a01 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/EnvDteExtensions.cs @@ -0,0 +1,96 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio +{ + using System.Globalization; + using EnvDTE; + using Microsoft.VisualStudio; + using Microsoft.VisualStudio.OLE.Interop; + using Microsoft.VisualStudio.Shell; + using Microsoft.VisualStudio.Shell.Interop; + + /// + /// Extension methods for types in the EnvDTE namespace. + /// + internal static class EnvDteExtensions + { + public static IVsHierarchy AsHierarchy(this Project project) + { + using (var serviceProvider = new ServiceProvider((IServiceProvider)project.DTE)) + { + var solution = (IVsSolution)serviceProvider.GetService(typeof(SVsSolution)); + + IVsHierarchy hierarchy; + ErrorHandler.ThrowOnFailure(solution.GetProjectOfUniqueName(project.UniqueName, out hierarchy)); + + return hierarchy; + } + } + + /// + /// Gets MSBuild metadata element of the specified project item. + /// + public static string GetItemAttribute(this ProjectItem projectItem, string attributeName) + { + IVsBuildPropertyStorage propertyStorage; + uint projectItemId; + GetBuildPropertyStorage(projectItem, out propertyStorage, out projectItemId); + + string value; + if (ErrorHandler.Failed(propertyStorage.GetItemAttribute(projectItemId, attributeName, out value))) + { + // Attribute doesn't exist + value = string.Empty; + } + + return value; + } + + public static uint GetItemId(this ProjectItem projectItem) + { + IVsHierarchy hierarchy = projectItem.ContainingProject.AsHierarchy(); + uint itemId; + ErrorHandler.ThrowOnFailure(hierarchy.ParseCanonicalName(projectItem.FileNames[1], out itemId)); + return itemId; + } + + /// + /// Sets MSBuild metadata element for the specified project item. + /// + public static void SetItemAttribute(this ProjectItem projectItem, string attributeName, string attributeValue) + { + IVsBuildPropertyStorage propertyStorage; + uint projectItemId; + GetBuildPropertyStorage(projectItem, out propertyStorage, out projectItemId); + + ErrorHandler.ThrowOnFailure(propertyStorage.SetItemAttribute(projectItemId, attributeName, attributeValue)); + } + + /// + /// Sets property value for the . + /// + /// + /// When the doesn't have a property with the specified . + /// + public static void SetPropertyValue(this ProjectItem projectItem, string propertyName, object propertyValue) + { + Property property = projectItem.Properties.Item(propertyName); + if (property == null) + { + throw new TransformationException( + string.Format(CultureInfo.CurrentCulture, "Property {0} is not supported for {1}", propertyName, projectItem.Name)); + } + + property.Value = propertyValue; + } + + private static void GetBuildPropertyStorage(ProjectItem projectItem, out IVsBuildPropertyStorage propertyStorage, out uint projectItemId) + { + IVsHierarchy hierarchy = projectItem.ContainingProject.AsHierarchy(); + propertyStorage = (IVsBuildPropertyStorage)hierarchy; + ErrorHandler.ThrowOnFailure(hierarchy.ParseCanonicalName(projectItem.FileNames[1], out projectItemId)); + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/GlobalSuppressions.cs b/src/T4Toolbox.VisualStudio/GlobalSuppressions.cs new file mode 100644 index 0000000..0f30172 Binary files /dev/null and b/src/T4Toolbox.VisualStudio/GlobalSuppressions.cs differ diff --git a/src/T4Toolbox.VisualStudio/ItemMetadataWizard.cs b/src/T4Toolbox.VisualStudio/ItemMetadataWizard.cs new file mode 100644 index 0000000..4fc9dbe --- /dev/null +++ b/src/T4Toolbox.VisualStudio/ItemMetadataWizard.cs @@ -0,0 +1,56 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio +{ + using System; + using System.Collections.Generic; + using System.Xml.Linq; + using EnvDTE; + + /// + /// Sets properties or MSBuild metadata for project item. + /// + /// + /// This wizard is similar to the ItemPropertiesWizard defined in the + /// Microsoft.VSDesigner.ProjectWizard namespace of the Microsoft.VSDesigner assembly. + /// It can be used for those project system implementations, such as SQLDB, that don't + /// support standard item properties. + /// + public class ItemMetadataWizard : ProjectItemTemplateWizard + { + /// + /// Sets properties or MSBuild metadata based on contents of the WizardData element of the .VSTemplate file. + /// + /// A that finished generating. + public override void ProjectItemFinishedGenerating(ProjectItem projectItem) + { + if (projectItem == null) + { + throw new ArgumentNullException("projectItem"); + } + + foreach (XElement metadata in this.GetMetadataItems()) + { + projectItem.SetItemAttribute(metadata.Name.LocalName, metadata.Value); + } + } + + /// + /// Returns attribute values stored in the .VSTEMPLATE file. + /// + /// An , where each item represents a single metadata element. + private IEnumerable GetMetadataItems() + { + string wizardData = this.ReplacementParameters["$wizarddata$"]; + XElement root = XDocument.Parse(wizardData).Root; + if (root == null) + { + throw new InvalidOperationException(); + } + + return root.Elements(); + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/OutputFileManager.cs b/src/T4Toolbox.VisualStudio/OutputFileManager.cs new file mode 100644 index 0000000..d3077fa --- /dev/null +++ b/src/T4Toolbox.VisualStudio/OutputFileManager.cs @@ -0,0 +1,780 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio +{ + using System; + using System.CodeDom.Compiler; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Runtime.InteropServices; + using System.Text; + using EnvDTE; + using EnvDTE80; + using Microsoft.Build.Execution; + using Microsoft.VisualStudio; + using Microsoft.VisualStudio.Shell.Interop; + using Microsoft.VisualStudio.TextTemplating; + using Microsoft.VisualStudio.TextTemplating.VSHost; + using VSLangProj; + + /// + /// Handles a single request to update output files. + /// + internal class OutputFileManager + { + private readonly DTE dte; + private readonly ProjectItem input; + private readonly string inputFile; + private readonly string inputDirectory; + private readonly OutputFile[] outputFiles; + private readonly IDictionary projects; + private readonly IServiceProvider serviceProvider; + private readonly ITextTemplatingEngineHost templatingHost; + + public OutputFileManager(IServiceProvider serviceProvider, string inputFile, OutputFile[] outputFiles) + { + this.serviceProvider = serviceProvider; + this.inputFile = inputFile; + this.inputDirectory = Path.GetDirectoryName(inputFile); + this.outputFiles = outputFiles; + this.dte = (DTE)serviceProvider.GetService(typeof(DTE)); + this.projects = GetAllProjects(this.dte.Solution); + this.input = this.dte.Solution.FindProjectItem(this.inputFile); + this.templatingHost = (ITextTemplatingEngineHost)this.serviceProvider.GetService(typeof(STextTemplating)); + } + + /// + /// Executes the logic necessary to update output files. + /// + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "How else do we report an error from a background task?")] + public void DoWork() + { + try + { + this.DeleteOldOutputs(); + List outputsToSave = this.GetOutputFilesToSave().ToList(); + this.CheckoutFiles(outputsToSave.Select(output => this.GetFullPath(output.Path)).ToArray()); + this.SaveOutputFiles(outputsToSave); + this.ConfigureOutputFiles(); + this.RecordLastOutputs(); + } + catch (TransformationException e) + { + // Expected error condition. Log message only. + this.LogError(e.Message); + } + catch (Exception e) + { + // Unexpected error. Log the whole thing, including its callstack. + this.LogError(e.ToString()); + } + } + + /// + /// Performs validation tasks that require accessing Visual Studio automation model. + /// + public void Validate() + { + foreach (OutputFile output in this.outputFiles) + { + Project project; + this.ValidateOutputProject(output, out project); + this.ValidateOutputDirectory(output, project); + ValidateOutputItemType(output, project); + this.ValidateOutputEncoding(output); + this.ValidateOutputContent(output); + } + } + + /// + /// Adds projects, recursively, from the specified to the collection. + /// + private static void AddAllProjects(Project solutionItem, IDictionary projects) + { + if (solutionItem.Kind == ProjectKinds.vsProjectKindSolutionFolder) + { + foreach (ProjectItem item in solutionItem.ProjectItems) + { + if (item.SubProject != null) + { + AddAllProjects(item.SubProject, projects); + } + } + } + else + { + try + { + projects.Add(solutionItem.FullName, solutionItem); + } + catch (NotImplementedException) + { + // Ignore projects that don't support FullName property. + } + } + } + + /// + /// Adds a folder to a specified of project items. + /// + /// + /// A collection that belongs to a or + /// of type . + /// + /// + /// Name of the folder to be added. + /// + /// + /// Absolute path to the directory where the folder is located. + /// + /// + /// A that represents new folder added to the . + /// + /// + /// If the specified folder doesn't exist in the solution and the file system, + /// a new folder will be created in both. However, if the specified folder + /// already exists in the file system, it will be added to the solution instead. + /// Unfortunately, an existing folder can only be added to the solution with + /// all of sub-folders and files in it. Thus, if a single output file is + /// generated in an existing folders not in the solution, the target folder will + /// be added to the solution with all files in it, generated or not. The + /// only way to avoid this would be to immediately remove all child items + /// from a newly added existing folder. However, this could lead to having + /// orphaned files that were added to source control and later excluded from + /// the project. We may need to revisit this code and access + /// automation model to remove the child items from source control too. + /// + private static ProjectItem AddFolder(ProjectItems collection, string folderName, string basePath) + { + // Does the folder already exist in the solution? + ProjectItem folder = collection.Cast().FirstOrDefault(p => string.Equals(p.Name, folderName, StringComparison.OrdinalIgnoreCase)); + if (folder != null) + { + return folder; + } + + try + { + // Try adding folder to the project. + // Note that this will work for existing folder in a Database project but not in C#. + return collection.AddFolder(folderName); + } + catch (COMException) + { + // If folder already exists on disk and the previous attempt to add failed + string folderPath = Path.Combine(basePath, folderName); + if (Directory.Exists(folderPath)) + { + // Try adding it from disk + // Note that this will work in a C# but is not implemented in Database projects. + return collection.AddFromDirectory(folderPath); + } + + throw; + } + } + + private static Exception CheckoutAbortedException() + { + return new TransformationException( + "The code generation cannot be completed because one or more files that must be modified cannot be changed. " + + "If the files are under source control, you may want to check them out; if the files are read-only on disk, " + + "you may want to change their attributes."); + } + + /// + /// Configures properties, metadata and references of the . + /// + private static void ConfigureProjectItem(ProjectItem outputItem, OutputFile output) + { + ConfigureProjectItemProperties(outputItem, output); + ConfigureProjectItemMetadata(outputItem, output); + ConfigureProjectItemReferences(outputItem, output); + } + + /// + /// Sets the metadata for the added to solution. + /// + private static void ConfigureProjectItemMetadata(ProjectItem projectItem, OutputFile output) + { + // Set build projerties for the target project item + foreach (KeyValuePair metadata in output.Metadata) + { + // Set well-known metadata items via ProjectItem.Properties for immediate effect in Visual Studio + switch (metadata.Key) + { + case ItemMetadata.CopyToOutputDirectory: + projectItem.SetPropertyValue(ProjectItemProperty.CopyToOutputDirectory, output.CopyToOutputDirectory); + continue; + + case ItemMetadata.CustomToolNamespace: + projectItem.SetPropertyValue(ProjectItemProperty.CustomToolNamespace, metadata.Value); + continue; + + case ItemMetadata.Generator: + projectItem.SetPropertyValue(ProjectItemProperty.CustomTool, metadata.Value); + continue; + } + + // Set all other metadata items + projectItem.SetItemAttribute(metadata.Key, metadata.Value); + } + } + + /// + /// Sets the known properties for the to be added to solution. + /// + private static void ConfigureProjectItemProperties(ProjectItem projectItem, OutputFile output) + { + if (!string.IsNullOrEmpty(output.ItemType)) + { + projectItem.SetPropertyValue(ProjectItemProperty.ItemType, output.ItemType); + } + } + + /// + /// Adds assembly references required by the project item to its containing project. + /// + private static void ConfigureProjectItemReferences(ProjectItem projectItem, OutputFile output) + { + if (output.References.Count > 0) + { + var project = projectItem.ContainingProject.Object as VSProject; + if (project == null) + { + throw new TransformationException(string.Format(CultureInfo.CurrentCulture, "Project {0} does not support references required by {1}", projectItem.ContainingProject.Name, projectItem.Name)); + } + + foreach (string reference in output.References) + { + try + { + project.References.Add(reference); + } + catch (COMException) + { + throw new TransformationException(string.Format(CultureInfo.CurrentCulture, "Reference {0} required by {1} could not be added to project {2}", reference, projectItem.Name, projectItem.ContainingProject.Name)); + } + } + } + } + + /// + /// Deletes the specified and its parent folders if they are empty. + /// + /// + /// A Visual Studio . + /// + /// + /// This method correctly deletes empty parent folders in C# and probably + /// Visual Basic projects which are implemented in C++ as pure COM objects. + /// However, for Database and probably WiX projects, which are implemented + /// as .NET COM objects, the parent collection indicates item count = 1 + /// even after its only child item is deleted. So, for new project types, + /// this method doesn't delete empty parent folders. However, this is probably + /// desirable for Database projects that create a predefined, empty folder + /// structure for each schema. We may need to solve this problem in the + /// future by recording which folders were actually created by the code + /// generator in the log file and deleting the empty parent folders when + /// the previously generated folders become empty. + /// + private static void DeleteProjectItem(ProjectItem item) + { + ProjectItems parentCollection = item.Collection; + + item.Delete(); + + if (parentCollection.Count == 0) + { + var parent = parentCollection.Parent as ProjectItem; + if (parent != null && parent.Kind == EnvDTE.Constants.vsProjectItemKindPhysicalFolder) + { + DeleteProjectItem(parent); + } + } + } + + /// + /// Retrieves projects from the specified . + /// + private static IDictionary GetAllProjects(Solution solution) + { + var projects = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (Project project in solution.Projects) + { + AddAllProjects(project, projects); + } + + return projects; + } + + /// + /// Returns a list of item types available in the specified . + /// + private static ICollection GetAvailableItemTypes(Project project) + { + var itemTypes = new List { ItemType.None, ItemType.Compile, ItemType.Content, ItemType.EmbeddedResource }; + + var projectInstance = new ProjectInstance(project.FullName); + foreach (ProjectItemInstance item in projectInstance.Items) + { + if (item.ItemType == "AvailableItemName") + { + itemTypes.Add(item.EvaluatedInclude); + } + } + + return itemTypes; + } + + private static bool IsEmptyOrWhiteSpace(StringBuilder text) + { + for (int i = 0; i < text.Length; i++) + { + if (!char.IsWhiteSpace(text[i])) + { + return false; + } + } + + return true; + } + + private static void ReloadDocument(IVsRunningDocumentTable runningDocumentTable, string outputFilePath) + { + if (runningDocumentTable == null) + { + // SVsRunningDocumentTable service is not available (as in a unit test). + return; + } + + IVsHierarchy hierarchy; + uint itemId; + IntPtr persistDocDataPointer; + uint cookie; + ErrorHandler.ThrowOnFailure(runningDocumentTable.FindAndLockDocument((uint)_VSRDTFLAGS.RDT_NoLock, outputFilePath, out hierarchy, out itemId, out persistDocDataPointer, out cookie)); + if (persistDocDataPointer == IntPtr.Zero) + { + // Document is not currently opened in Visual Studio editor. + return; + } + + var persistDocData = (IVsPersistDocData)Marshal.GetObjectForIUnknown(persistDocDataPointer); + ErrorHandler.ThrowOnFailure(persistDocData.ReloadDocData((uint)(_VSRELOADDOCDATA.RDD_IgnoreNextFileChange | _VSRELOADDOCDATA.RDD_RemoveUndoStack))); + } + + /// + /// Determines whether two project items collections are the same. + /// + /// + /// First collection. + /// + /// + /// Second collection. + /// + /// True, if the two collections are the same. + /// + /// This method is necessary for MPF-based project implementations, such as database projects, which can return different + /// ProjectItems instances ultimately pointing to the same folder. + /// + private static bool Same(ProjectItems collection1, ProjectItems collection2) + { + if (collection1 == collection2) + { + return true; + } + + var parentItem1 = collection1.Parent as ProjectItem; + var parentItem2 = collection2.Parent as ProjectItem; + if (parentItem1 != null && parentItem2 != null) + { + if (!string.Equals(parentItem1.Name, parentItem2.Name, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return Same(parentItem1.Collection, parentItem2.Collection); + } + + var parentProject1 = collection1.Parent as Project; + var parentProject2 = collection2.Parent as Project; + if (parentProject1 != null && parentProject2 != null) + { + return string.Equals(parentProject1.FullName, parentProject2.FullName, StringComparison.OrdinalIgnoreCase); + } + + return false; + } + + private static void ValidateOutputItemType(OutputFile output, Project outputProject) + { + if (!string.IsNullOrEmpty(output.ItemType)) + { + ICollection itemTypes = GetAvailableItemTypes(outputProject); + if (!itemTypes.Contains(output.ItemType)) + { + throw new TransformationException(string.Format(CultureInfo.CurrentCulture, "ItemType {0} specified for output file {1} is not supported for project {2}", output.ItemType, output.Path, outputProject.FullName)); + } + } + } + + /// + /// Deletes output files that were not generated by the current session. + /// + private void DeleteOldOutputs() + { + string lastOutputs = this.input.GetItemAttribute(ItemMetadata.LastOutputs); + + // Delete all files recorded in the log that were not regenerated + string[] logEntries = lastOutputs.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); + foreach (string line in logEntries) + { + string relativePath = line.Trim(); + + // Skip blank lines + if (relativePath.Length == 0) + { + continue; + } + + string absolutePath = this.GetFullPath(relativePath); + + // Skip the file if it was regenerated during current transformation + if (this.outputFiles.Any(output => string.Equals(this.GetFullPath(output.Path), absolutePath, StringComparison.OrdinalIgnoreCase))) + { + continue; + } + + // The file wasn't regenerated, delete it from the solution, source control and file storage + ProjectItem projectItem = this.dte.Solution.FindProjectItem(absolutePath); + if (projectItem != null) + { + DeleteProjectItem(projectItem); + } + } + } + + /// + /// Finds project item collection for the output file in the currently loaded Visual Studio solution. + /// + /// + /// An that needs to be added to the solution. + /// + /// + /// A collection where the generated file should be added. + /// + private ProjectItems FindProjectItemCollection(OutputFile output) + { + string outputFilePath = this.GetFullPath(output.Path); + ProjectItems collection; // collection to which output file needs to be added + string relativePath; // path from the collection to the file + string basePath; // absolute path to the directory to which an item is being added + + if (!string.IsNullOrEmpty(output.Project)) + { + // If output file needs to be added to another project + Project project = this.projects[this.GetFullPath(output.Project)]; + collection = project.ProjectItems; + relativePath = FileMethods.GetRelativePath(project.FullName, outputFilePath); + basePath = Path.GetDirectoryName(project.FullName); + } + else if (!string.IsNullOrEmpty(output.Directory)) + { + // If output file needs to be added to another folder of the current project + collection = this.input.ContainingProject.ProjectItems; + relativePath = FileMethods.GetRelativePath(this.input.ContainingProject.FullName, outputFilePath); + basePath = Path.GetDirectoryName(this.input.ContainingProject.FullName); + } + else + { + // Add the output file to the list of children of the input file + collection = this.input.ProjectItems; + relativePath = FileMethods.GetRelativePath(this.inputFile, outputFilePath); + basePath = Path.GetDirectoryName(this.inputFile); + } + + // make sure that all folders in the file path exist in the project. + if (relativePath.StartsWith("." + Path.DirectorySeparatorChar, StringComparison.Ordinal)) + { + // Remove leading .\ from the path + relativePath = relativePath.Substring(relativePath.IndexOf(Path.DirectorySeparatorChar) + 1); + + while (relativePath.Contains(Path.DirectorySeparatorChar)) + { + string folderName = relativePath.Substring(0, relativePath.IndexOf(Path.DirectorySeparatorChar)); + ProjectItem folder = AddFolder(collection, folderName, basePath); + + collection = folder.ProjectItems; + relativePath = relativePath.Substring(folderName.Length + 1); + basePath = Path.Combine(basePath, folderName); + } + } + + return collection; + } + + private string GetFullPath(string path) + { + if (!Path.IsPathRooted(path)) + { + path = Path.Combine(this.inputDirectory, path); + } + + return Path.GetFullPath(path); + } + + private string GetLastGenOutputFullPath() + { + string relativePath = this.input.GetItemAttribute(ItemMetadata.LastGenOutput); + if (!string.IsNullOrEmpty(relativePath)) + { + string projectDirectory = Path.GetDirectoryName(this.input.ContainingProject.FullName); + return Path.GetFullPath(Path.Combine(projectDirectory, relativePath)); + } + + return string.Empty; + } + + private void LogError(string message) + { + this.templatingHost.LogErrors(new CompilerErrorCollection { new CompilerError { ErrorText = message, FileName = this.inputFile } }); + } + + private void LogWarning(string message) + { + this.templatingHost.LogErrors(new CompilerErrorCollection { new CompilerError { ErrorText = message, FileName = this.inputFile, IsWarning = true } }); + } + + /// + /// Records a list of the generated files in the LastOutputs metadata of the input item. + /// + private void RecordLastOutputs() + { + string lastGenOutputFullPath = this.GetLastGenOutputFullPath(); + + // Create a list of files that may be regenerated/overwritten in the future + var outputFileList = new List(); + foreach (OutputFile output in this.outputFiles) + { + // Don't store the name of file user wants to preserve so that we don't deleted next time. + if (output.PreserveExistingFile) + { + continue; + } + + // Don't store the name of default output file second time + string outputFullPath = this.GetFullPath(output.Path); + if (string.Equals(lastGenOutputFullPath, outputFullPath, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + // Store a relative path from the input file to the output file + outputFileList.Add(FileMethods.GetRelativePath(this.inputFile, outputFullPath)); + } + + // If more than one output file was generated, write one file per line in alphabetical order for readability + outputFileList.Sort(); + string lastOutputs = string.Join(Environment.NewLine, outputFileList); + if (outputFileList.Count > 1) + { + lastOutputs = Environment.NewLine + lastOutputs + Environment.NewLine; + } + + // Write the file list to the project file + this.input.SetItemAttribute(ItemMetadata.LastOutputs, lastOutputs); + } + + private IEnumerable GetOutputFilesToSave() + { + foreach (OutputFile output in this.outputFiles) + { + string outputFilePath = this.GetFullPath(output.Path); + + // Don't do anything unless the output file has changed and needs to be overwritten + if (File.Exists(outputFilePath)) + { + if (output.PreserveExistingFile || output.Content.ToString() == File.ReadAllText(outputFilePath, output.Encoding)) + { + continue; + } + } + + yield return output; + } + } + + private void SaveOutputFiles(IEnumerable outputsToSave) + { + var runningDocumentTable = (IVsRunningDocumentTable)this.serviceProvider.GetService(typeof(SVsRunningDocumentTable)); + foreach (OutputFile output in outputsToSave) + { + string outputFilePath = this.GetFullPath(output.Path); + Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)); + File.WriteAllText(outputFilePath, output.Content.ToString(), output.Encoding); + ReloadDocument(runningDocumentTable, outputFilePath); + } + } + + /// + /// Uses the service to checkout specified files with a minimum number of visual prompts. + /// + private void CheckoutFiles(string[] filePaths) + { + var queryService = (IVsQueryEditQuerySave2)this.serviceProvider.GetService(typeof(SVsQueryEditQuerySave)); + if (queryService == null) + { + // SVsQueryEditQueryService is not available, don't try to check out files. + return; + } + + // Call QueryEditFiles to perform the action specified in the Source Control/Editing setting of the Visual Studio Options dialog. + // Although, technically, we are not "editing" the generated files, we call this method because, unlike QuerySaveFiles, it displays + // a single visual prompt for all files that need to be checked out. + uint editInfo; + uint editResult; + ErrorHandler.ThrowOnFailure(queryService.QueryEditFiles((uint)tagVSQueryEditFlags.QEF_DisallowInMemoryEdits, filePaths.Length, filePaths, null, null, out editResult, out editInfo)); + if (editResult == (uint)tagVSQueryEditResult.QER_EditOK) + { + return; + } + + if (editResult == (uint)tagVSQueryEditResult.QER_NoEdit_UserCanceled && + (editInfo & (uint)tagVSQueryEditResultFlags.QER_CheckoutCanceledOrFailed) == (uint)tagVSQueryEditResultFlags.QER_CheckoutCanceledOrFailed) + { + throw CheckoutAbortedException(); + } + + // If QueryEditFiles did not allow us to modify the generated files, call QuerySaveFiles to perform the action specified in the + // Source Control/Saving setting of the Visual Studio Options dialog. + ErrorHandler.ThrowOnFailure(queryService.BeginQuerySaveBatch()); // Allow the user to cancel check-out-on-save for all files in the batch + try + { + uint saveResult; + ErrorHandler.ThrowOnFailure(queryService.QuerySaveFiles(0, filePaths.Length, filePaths, null, null, out saveResult)); + if (saveResult != (uint)tagVSQuerySaveResult.QSR_SaveOK) + { + throw CheckoutAbortedException(); + } + } + finally + { + ErrorHandler.ThrowOnFailure(queryService.EndQuerySaveBatch()); + } + } + + /// + /// Saves and configures the additional output created by the transformation. + /// + /// + /// Note that this method currently cannot distinguish between files that are + /// already in a Database project and files that are simply displayed with + /// "Show All Files" option. Database project model makes these items appear + /// as if they were included in the project. + /// + private void ConfigureOutputFile(OutputFile output) + { + string outputFilePath = this.GetFullPath(output.Path); + + ProjectItem outputItem = this.dte.Solution.FindProjectItem(outputFilePath); + ProjectItems collection = this.FindProjectItemCollection(output); + + if (outputItem == null) + { + // If output file has not been added to the solution + outputItem = collection.AddFromFile(outputFilePath); + } + else if (!Same(outputItem.Collection, collection)) + { + // If the output file moved from one collection to another + string backupFile = outputFilePath + ".bak"; + File.Move(outputFilePath, backupFile); // Prevent unnecessary source control operations + outputItem.Delete(); // Remove doesn't work on "DependentUpon" items + File.Move(backupFile, outputFilePath); + + outputItem = collection.AddFromFile(outputFilePath); + } + + ConfigureProjectItem(outputItem, output); + } + + /// + /// Saves output files, creates and configures project items. + /// + private void ConfigureOutputFiles() + { + foreach (OutputFile output in this.outputFiles) + { + this.ConfigureOutputFile(output); + } + } + + private void ValidateOutputContent(OutputFile output) + { + // If additional output file is empty, warn the user to encourage them to cleanup their code generator + if (!string.IsNullOrEmpty(output.File) && IsEmptyOrWhiteSpace(output.Content)) + { + this.LogWarning(string.Format(CultureInfo.CurrentCulture, "Generated output file '{0}' is empty.", output.Path)); + } + } + + private void ValidateOutputDirectory(OutputFile output, Project outputProject) + { + if (!string.IsNullOrEmpty(output.Directory)) + { + string projectPath = Path.GetDirectoryName(outputProject.FullName); + string outputPath = this.GetFullPath(output.Path); + if (!outputPath.StartsWith(projectPath, StringComparison.OrdinalIgnoreCase)) + { + throw new TransformationException(string.Format(CultureInfo.CurrentCulture, "Output file {0} is located outside of directory of target project {1}", outputPath, outputProject.FullName)); + } + } + } + + private void ValidateOutputEncoding(OutputFile output) + { + if (string.IsNullOrEmpty(output.File)) + { + object service = this.serviceProvider.GetService(typeof(STextTemplating)); + + // Try to change the encoding + var host = (ITextTemplatingEngineHost)service; + host.SetOutputEncoding(output.Encoding, false); + + // Check if the encoding was already set by the output directive and cannot be changed + var components = (ITextTemplatingComponents)service; + var callback = components.Callback as TextTemplatingCallback; // Callback can be provided by user code, not only by T4. + if (callback != null && !object.Equals(callback.OutputEncoding, output.Encoding)) + { + throw new TransformationException( + string.Format( + CultureInfo.CurrentCulture, + "Encoding value {0} does not match value {1} set by the output directive.", + output.Encoding.EncodingName, + callback.OutputEncoding.EncodingName)); + } + } + } + + private void ValidateOutputProject(OutputFile output, out Project project) + { + if (string.IsNullOrEmpty(output.Project)) + { + project = this.input.ContainingProject; + } + else + { + if (!this.projects.TryGetValue(this.GetFullPath(output.Project), out project)) + { + throw new TransformationException(string.Format(CultureInfo.CurrentCulture, "Target project {0} does not belong to the solution", output.Project)); + } + } + } + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/ProjectItemProperty.cs b/src/T4Toolbox.VisualStudio/ProjectItemProperty.cs new file mode 100644 index 0000000..2792d88 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/ProjectItemProperty.cs @@ -0,0 +1,42 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio +{ + /// + /// Defines constants for commonly-used VisualStudio project item properties. + /// + internal static class ProjectItemProperty + { + /// + /// Internal name of the "Copy to Output Directory" project item property. + /// + public const string CopyToOutputDirectory = "CopyToOutputDirectory"; + + /// + /// Internal name of the "Custom Tool" project item property. + /// + public const string CustomTool = "CustomTool"; + + /// + /// Internal name of the "Custom Tool Namespace" project item property. + /// + public const string CustomToolNamespace = "CustomToolNamespace"; + + /// + /// Internal name of the "Custom Tool Parameters" project item property provided by the T4 Toolbox. + /// + public const string CustomToolParameters = BrowseObjectExtender.Name + ".CustomToolParameters"; + + /// + /// Internal name of the "Custom Tool Template" project item property provided by the T4 Toolbox. + /// + public const string CustomToolTemplate = BrowseObjectExtender.Name + ".CustomToolTemplate"; + + /// + /// Internal name of the "Build Action" project item property. + /// + public const string ItemType = "ItemType"; + } +} \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/ProjectItemTemplateWizard.cs b/src/T4Toolbox.VisualStudio/ProjectItemTemplateWizard.cs new file mode 100644 index 0000000..3f9c61f --- /dev/null +++ b/src/T4Toolbox.VisualStudio/ProjectItemTemplateWizard.cs @@ -0,0 +1,119 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio +{ + using System; + using System.Collections.Generic; + using EnvDTE; + using Microsoft.VisualStudio.TemplateWizard; + + /// + /// Serves as a base class for project item template wizards. Provides empty + /// implementations for all methods except . + /// + public abstract class ProjectItemTemplateWizard : IWizard + { + /// + /// Stores the replacements dictionary obtained in the method. + /// + private Dictionary replacementParameters; + + /// + /// Gets a dictionary of replacement parameters. + /// + /// + /// A of parameter values indexed by parameter name. + /// + protected Dictionary ReplacementParameters + { + get { return this.replacementParameters; } + } + + /// + /// Runs custom wizard logic before opening an item in the template. + /// + /// + /// The that will be opened. + /// + /// + /// This method is intentionally left blank. + /// + public void BeforeOpeningFile(ProjectItem projectItem) + { + } + + /// + /// Runs custom wizard logic when a project has finished generating. + /// + /// + /// The that finished generating. + /// + /// + /// This method is intentionally left blank. + /// + public void ProjectFinishedGenerating(Project project) + { + } + + /// + /// Runs custom wizard logic when a project item has finished generating. + /// + /// + /// The project item that finished generating. + /// + public abstract void ProjectItemFinishedGenerating(ProjectItem projectItem); + + /// + /// Runs custom wizard logic when the wizard has completed all tasks. + /// + /// + /// This method performs cleanup at the end of template unfolding. + /// + public virtual void RunFinished() + { + this.replacementParameters = null; + } + + /// + /// Runs custom wizard logic at the beginning of a template wizard run. + /// + /// + /// The automation object being used by the template wizard. + /// + /// + /// The list of standard parameters to be replaced. + /// + /// + /// Indicating the type of wizard run. + /// + /// + /// The custom parameters with which to perform parameter replacement in the project. + /// + /// + /// This method initializes dictionary. + /// + public virtual void RunStarted(object automationObject, Dictionary replacementsDictionary, WizardRunKind runKind, object[] customParams) + { + this.replacementParameters = replacementsDictionary; + } + + /// + /// Indicates whether the specified project item should be added to the project. + /// + /// + /// The path to the project item. + /// + /// + /// True if the project item should be added to the project; otherwise, false. + /// + /// + /// This method always returns true. + /// + public bool ShouldAddProjectItem(string filePath) + { + return true; + } + } +} diff --git a/src/T4Toolbox.VisualStudio/Resources.Designer.cs b/src/T4Toolbox.VisualStudio/Resources.Designer.cs new file mode 100644 index 0000000..4ad45a4 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Resources.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.18033 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace T4Toolbox.VisualStudio { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("T4Toolbox.VisualStudio.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to T4 Toolbox. + /// + internal static string _100 { + get { + return ResourceManager.GetString("100", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to General. + /// + internal static string _101 { + get { + return ResourceManager.GetString("101", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Path must be absolute.. + /// + internal static string InputFilePathMustBeAbsoluteMessage { + get { + return ResourceManager.GetString("InputFilePathMustBeAbsoluteMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to One of the output files is a null reference. + /// + internal static string OutputFileIsNullMessage { + get { + return ResourceManager.GetString("OutputFileIsNullMessage", resourceCulture); + } + } + } +} diff --git a/src/T4Toolbox.VisualStudio/Resources.resx b/src/T4Toolbox.VisualStudio/Resources.resx new file mode 100644 index 0000000..e6a00fd --- /dev/null +++ b/src/T4Toolbox.VisualStudio/Resources.resx @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + T4 Toolbox + Category of the T4 Toolbox page in the Visual Studio Options dialog + + + General + Name of the T4 Toolbox page in Visual Studio Options dialog + + + Path must be absolute. + + + One of the output files is a null reference + + \ No newline at end of file diff --git a/src/T4Toolbox.VisualStudio/ScriptFileGenerator.cs b/src/T4Toolbox.VisualStudio/ScriptFileGenerator.cs new file mode 100644 index 0000000..4969a10 --- /dev/null +++ b/src/T4Toolbox.VisualStudio/ScriptFileGenerator.cs @@ -0,0 +1,126 @@ +// +// Copyright © Oleg Sych. All Rights Reserved. +// + +namespace T4Toolbox.VisualStudio +{ + using System; + using System.IO; + using System.Linq; + using System.Runtime.InteropServices; + using System.Text; + using EnvDTE; + using Microsoft.VisualStudio; + using Microsoft.VisualStudio.Designer.Interfaces; + using Microsoft.VisualStudio.Shell.Interop; + using Microsoft.VisualStudio.TextTemplating.VSHost; + + /// + /// Two-stage template-based file generator. + /// + /// + /// When associated with any file, this generator will produce an empty text template + /// with the same name as the file and .tt extension. This template will be then + /// transformed by the standard TextTemplatingFileGenerator. If the template already + /// exist, this generator will preserve its content and still trigger the second + /// code generation stage. + /// + [Guid("8CAB1895-2287-463F-BE14-1ADB873B4741")] + public class ScriptFileGenerator : BaseCodeGeneratorWithSite + { + internal const string Name = "T4Toolbox.ScriptFileGenerator"; + internal const string Description = "Generator that creates a new or transforms existing Text Template"; + + /// + /// Returns extension of the output file this generator produces. + /// + public override string GetDefaultExtension() + { + return ".tt"; + } + + /// + /// Generates new or transforms existing T4 script. + /// + protected override byte[] GenerateCode(string inputFileName, string inputFileContent) + { + return + this.GenerateFromAssociatedTemplateFile(inputFileName) ?? + this.GenerateFromExistingScriptFile(inputFileName) ?? + this.GenerateNewScriptFile(inputFileName); + } + + private byte[] GenerateFromExistingScriptFile(string inputFileName) + { + string outputFileName = Path.ChangeExtension(inputFileName, this.GetDefaultExtension()); + if (File.Exists(outputFileName)) + { + // If the output file is opened in Visual Studio editor, save it to prevent the "Run Custom Tool" implementation from silently discarding changes. + Document outputDocument = this.Dte.Documents.Cast().SingleOrDefault(d => d.FullName == outputFileName); + if (outputDocument != null && !outputDocument.Saved) + { + // Save the script file if it was modified + outputDocument.Save(string.Empty); + } + + // Read it from disk. The "Run Custom Tool" implementation always overwrites it. + return File.ReadAllBytes(outputFileName); + } + + return null; + } + + private byte[] GenerateFromAssociatedTemplateFile(string inputFileName) + { + var hierarchy = (IVsHierarchy)this.GetService(typeof(IVsHierarchy)); + + uint inputItemId; + ErrorHandler.ThrowOnFailure(hierarchy.ParseCanonicalName(inputFileName, out inputItemId)); + + string templatePath; + var propertyStorage = (IVsBuildPropertyStorage)hierarchy; + if (ErrorHandler.Failed(propertyStorage.GetItemAttribute(inputItemId, ItemMetadata.Template, out templatePath))) + { + return null; + } + + // Remove