Cosmos/source/Archive/MPF/12.0/SingleFileGenerator.cs

554 lines
24 KiB
C#

/********************************************************************************************
Copyright (c) Microsoft Corporation
All rights reserved.
Microsoft Public License:
This license governs use of the accompanying software. If you use the software, you
accept this license. If you do not accept the license, do not use the software.
1. Definitions
The terms "reproduce," "reproduction," "derivative works," and "distribution" have the
same meaning here as under U.S. copyright law.
A "contribution" is the original software, or any additions or changes to the software.
A "contributor" is any person that distributes its contribution under this license.
"Licensed patents" are a contributor's patent claims that read directly on its contribution.
2. Grant of Rights
(A) Copyright Grant- Subject to the terms of this license, including the license conditions
and limitations in section 3, each contributor grants you a non-exclusive, worldwide,
royalty-free copyright license to reproduce its contribution, prepare derivative works of
its contribution, and distribute its contribution or any derivative works that you create.
(B) Patent Grant- Subject to the terms of this license, including the license conditions
and limitations in section 3, each contributor grants you a non-exclusive, worldwide,
royalty-free license under its licensed patents to make, have made, use, sell, offer for
sale, import, and/or otherwise dispose of its contribution in the software or derivative
works of the contribution in the software.
3. Conditions and Limitations
(A) No Trademark License- This license does not grant you rights to use any contributors'
name, logo, or trademarks.
(B) If you bring a patent claim against any contributor over patents that you claim are
infringed by the software, your patent license from such contributor to the software ends
automatically.
(C) If you distribute any portion of the software, you must retain all copyright, patent,
trademark, and attribution notices that are present in the software.
(D) If you distribute any portion of the software in source code form, you may do so only
under this license by including a complete copy of this license with your distribution.
If you distribute any portion of the software in compiled or object code form, you may only
do so under a license that complies with this license.
(E) The software is licensed "as-is." You bear the risk of using it. The contributors give
no express warranties, guarantees or conditions. You may have additional consumer rights
under your local laws which this license cannot change. To the extent permitted under your
local laws, the contributors exclude the implied warranties of merchantability, fitness for
a particular purpose and non-infringement.
********************************************************************************************/
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.TextManager.Interop;
namespace Microsoft.VisualStudio.Project
{
/// <summary>
/// Provides support for single file generator.
/// </summary>
internal class SingleFileGenerator : ISingleFileGenerator, IVsGeneratorProgress
{
#region fields
private bool gettingCheckoutStatus;
private bool runningGenerator;
private ProjectNode projectMgr;
#endregion
#region ctors
/// <summary>
/// Overloadde ctor.
/// </summary>
/// <param name="ProjectNode">The associated project</param>
internal SingleFileGenerator(ProjectNode projectMgr)
{
this.projectMgr = projectMgr;
}
#endregion
#region IVsGeneratorProgress Members
public virtual int GeneratorError(int warning, uint level, string err, uint line, uint col)
{
return VSConstants.E_NOTIMPL;
}
public virtual int Progress(uint complete, uint total)
{
return VSConstants.E_NOTIMPL;
}
#endregion
#region ISingleFileGenerator
/// <summary>
/// Runs the generator on the current project item.
/// </summary>
/// <param name="document"></param>
/// <returns></returns>
public virtual void RunGenerator(string document)
{
// Go run the generator on that node, but only if the file is dirty
// in the running document table. Otherwise there is no need to rerun
// the generator because if the original document is not dirty then
// the generated output should be already up to date.
uint itemid = VSConstants.VSITEMID_NIL;
IVsHierarchy hier = (IVsHierarchy)this.projectMgr;
if(document != null && hier != null && ErrorHandler.Succeeded(hier.ParseCanonicalName((string)document, out itemid)))
{
IVsHierarchy rdtHier;
IVsPersistDocData perDocData;
uint cookie;
if(this.VerifyFileDirtyInRdt((string)document, out rdtHier, out perDocData, out cookie))
{
// Run the generator on the indicated document
FileNode node = (FileNode)this.projectMgr.NodeFromItemId(itemid);
this.InvokeGenerator(node);
}
}
}
#endregion
#region virtual methods
/// <summary>
/// Invokes the specified generator
/// </summary>
/// <param name="fileNode">The node on which to invoke the generator.</param>
protected internal virtual void InvokeGenerator(FileNode fileNode)
{
if(fileNode == null)
{
throw new ArgumentNullException("fileNode");
}
SingleFileGeneratorNodeProperties nodeproperties = fileNode.NodeProperties as SingleFileGeneratorNodeProperties;
if(nodeproperties == null)
{
throw new InvalidOperationException();
}
string customToolProgID = nodeproperties.CustomTool;
if(string.IsNullOrEmpty(customToolProgID))
{
return;
}
string customToolNamespace = nodeproperties.CustomToolNamespace;
try
{
if(!this.runningGenerator)
{
//Get the buffer contents for the current node
string moniker = fileNode.GetMkDocument();
this.runningGenerator = true;
//Get the generator
IVsSingleFileGenerator generator;
int generateDesignTimeSource;
int generateSharedDesignTimeSource;
int generateTempPE;
SingleFileGeneratorFactory factory = new SingleFileGeneratorFactory(this.projectMgr.ProjectGuid, this.projectMgr.Site);
ErrorHandler.ThrowOnFailure(factory.CreateGeneratorInstance(customToolProgID, out generateDesignTimeSource, out generateSharedDesignTimeSource, out generateTempPE, out generator));
//Check to see if the generator supports siting
IObjectWithSite objWithSite = generator as IObjectWithSite;
if(objWithSite != null)
{
objWithSite.SetSite(fileNode.OleServiceProvider);
}
//Determine the namespace
if(string.IsNullOrEmpty(customToolNamespace))
{
customToolNamespace = this.ComputeNamespace(moniker);
}
//Run the generator
IntPtr[] output = new IntPtr[1];
output[0] = IntPtr.Zero;
uint outPutSize;
string extension;
ErrorHandler.ThrowOnFailure(generator.DefaultExtension(out extension));
//Find if any dependent node exists
string dependentNodeName = Path.GetFileNameWithoutExtension(fileNode.FileName) + extension;
HierarchyNode dependentNode = fileNode.FirstChild;
while(dependentNode != null)
{
if(string.Compare(dependentNode.ItemNode.GetMetadata(ProjectFileConstants.DependentUpon), fileNode.FileName, StringComparison.OrdinalIgnoreCase) == 0)
{
dependentNodeName = ((FileNode)dependentNode).FileName;
break;
}
dependentNode = dependentNode.NextSibling;
}
//If you found a dependent node.
if(dependentNode != null)
{
//Then check out the node and dependent node from SCC
if(!this.CanEditFile(dependentNode.GetMkDocument()))
{
throw Marshal.GetExceptionForHR(VSConstants.OLE_E_PROMPTSAVECANCELLED);
}
}
else //It is a new node to be added to the project
{
// Check out the project file if necessary.
if(!this.projectMgr.QueryEditProjectFile(false))
{
throw Marshal.GetExceptionForHR(VSConstants.OLE_E_PROMPTSAVECANCELLED);
}
}
IVsTextStream stream;
string inputFileContents = this.GetBufferContents(moniker, out stream);
ErrorHandler.ThrowOnFailure(generator.Generate(moniker, inputFileContents, customToolNamespace, output, out outPutSize, this));
byte[] data = new byte[outPutSize];
if(output[0] != IntPtr.Zero)
{
Marshal.Copy(output[0], data, 0, (int)outPutSize);
Marshal.FreeCoTaskMem(output[0]);
}
//Todo - Create a file and add it to the Project
this.UpdateGeneratedCodeFile(fileNode, data, (int)outPutSize, dependentNodeName);
}
}
finally
{
this.runningGenerator = false;
}
}
/// <summary>
/// Computes the names space based on the folder for the ProjectItem. It just replaces DirectorySeparatorCharacter
/// with "." for the directory in which the file is located.
/// </summary>
/// <returns>Returns the computed name space</returns>
protected virtual string ComputeNamespace(string projectItemPath)
{
if(String.IsNullOrEmpty(projectItemPath))
{
throw new ArgumentException(SR.GetString(SR.ParameterCannotBeNullOrEmpty, CultureInfo.CurrentUICulture), "projectItemPath");
}
string nspace = "";
string filePath = Path.GetDirectoryName(projectItemPath);
string[] toks = filePath.Split(new char[] { ':', '\\' });
foreach(string tok in toks)
{
if(!String.IsNullOrEmpty(tok))
{
string temp = tok.Replace(" ", "");
nspace += (temp + ".");
}
}
nspace = nspace.Remove(nspace.LastIndexOf(".", StringComparison.Ordinal), 1);
return nspace;
}
/// <summary>
/// This is called after the single file generator has been invoked to create or update the code file.
/// </summary>
/// <param name="fileNode">The node associated to the generator</param>
/// <param name="data">data to update the file with</param>
/// <param name="size">size of the data</param>
/// <param name="fileName">Name of the file to update or create</param>
/// <returns>full path of the file</returns>
protected virtual string UpdateGeneratedCodeFile(FileNode fileNode, byte[] data, int size, string fileName)
{
string filePath = Path.Combine(Path.GetDirectoryName(fileNode.GetMkDocument()), fileName);
IVsRunningDocumentTable rdt = this.projectMgr.GetService(typeof(SVsRunningDocumentTable)) as IVsRunningDocumentTable;
// (kberes) Shouldn't this be an InvalidOperationException instead with some not to annoying errormessage to the user?
if(rdt == null)
{
ErrorHandler.ThrowOnFailure(VSConstants.E_FAIL);
}
IVsHierarchy hier;
uint cookie;
uint itemid;
IntPtr docData = IntPtr.Zero;
ErrorHandler.ThrowOnFailure(rdt.FindAndLockDocument((uint)(_VSRDTFLAGS.RDT_NoLock), filePath, out hier, out itemid, out docData, out cookie));
if(docData != IntPtr.Zero)
{
Marshal.Release(docData);
IVsTextStream srpStream = null;
if(srpStream != null)
{
int oldLen = 0;
int hr = srpStream.GetSize(out oldLen);
if(ErrorHandler.Succeeded(hr))
{
IntPtr dest = IntPtr.Zero;
try
{
dest = Marshal.AllocCoTaskMem(data.Length);
Marshal.Copy(data, 0, dest, data.Length);
ErrorHandler.ThrowOnFailure(srpStream.ReplaceStream(0, oldLen, dest, size / 2));
}
finally
{
if(dest != IntPtr.Zero)
{
Marshal.Release(dest);
}
}
}
}
}
else
{
using(FileStream generatedFileStream = File.Open(filePath, FileMode.OpenOrCreate))
{
generatedFileStream.Write(data, 0, size);
}
EnvDTE.ProjectItem projectItem = fileNode.GetAutomationObject() as EnvDTE.ProjectItem;
if(projectItem != null && (this.projectMgr.FindChild(fileNode.FileName) == null))
{
projectItem.ProjectItems.AddFromFile(filePath);
}
}
return filePath;
}
#endregion
#region helpers
/// <summary>
/// Returns the buffer contents for a moniker.
/// </summary>
/// <returns>Buffer contents</returns>
private string GetBufferContents(string fileName, out IVsTextStream srpStream)
{
Guid CLSID_VsTextBuffer = new Guid("{8E7B96A8-E33D-11d0-A6D5-00C04FB67F6A}");
string bufferContents = "";
srpStream = null;
IVsRunningDocumentTable rdt = this.projectMgr.GetService(typeof(SVsRunningDocumentTable)) as IVsRunningDocumentTable;
if(rdt != null)
{
IVsHierarchy hier;
IVsPersistDocData persistDocData;
uint itemid, cookie;
bool docInRdt = true;
IntPtr docData = IntPtr.Zero;
int hr = NativeMethods.E_FAIL;
try
{
//Getting a read lock on the document. Must be released later.
hr = rdt.FindAndLockDocument((uint)_VSRDTFLAGS.RDT_ReadLock, fileName, out hier, out itemid, out docData, out cookie);
if(ErrorHandler.Failed(hr) || docData == IntPtr.Zero)
{
Guid iid = VSConstants.IID_IUnknown;
cookie = 0;
docInRdt = false;
ILocalRegistry localReg = this.projectMgr.GetService(typeof(SLocalRegistry)) as ILocalRegistry;
ErrorHandler.ThrowOnFailure(localReg.CreateInstance(CLSID_VsTextBuffer, null, ref iid, (uint)CLSCTX.CLSCTX_INPROC_SERVER, out docData));
}
persistDocData = Marshal.GetObjectForIUnknown(docData) as IVsPersistDocData;
}
finally
{
if(docData != IntPtr.Zero)
{
Marshal.Release(docData);
}
}
//Try to get the Text lines
IVsTextLines srpTextLines = persistDocData as IVsTextLines;
if(srpTextLines == null)
{
// Try getting a text buffer provider first
IVsTextBufferProvider srpTextBufferProvider = persistDocData as IVsTextBufferProvider;
if(srpTextBufferProvider != null)
{
hr = srpTextBufferProvider.GetTextBuffer(out srpTextLines);
}
}
if(ErrorHandler.Succeeded(hr))
{
srpStream = srpTextLines as IVsTextStream;
if(srpStream != null)
{
// QI for IVsBatchUpdate and call FlushPendingUpdates if they support it
IVsBatchUpdate srpBatchUpdate = srpStream as IVsBatchUpdate;
if(srpBatchUpdate != null)
ErrorHandler.ThrowOnFailure(srpBatchUpdate.FlushPendingUpdates(0));
int lBufferSize = 0;
hr = srpStream.GetSize(out lBufferSize);
if(ErrorHandler.Succeeded(hr))
{
IntPtr dest = IntPtr.Zero;
try
{
// Note that GetStream returns Unicode to us so we don't need to do any conversions
dest = Marshal.AllocCoTaskMem((lBufferSize + 1) * 2);
ErrorHandler.ThrowOnFailure(srpStream.GetStream(0, lBufferSize, dest));
//Get the contents
bufferContents = Marshal.PtrToStringUni(dest);
}
finally
{
if(dest != IntPtr.Zero)
Marshal.FreeCoTaskMem(dest);
}
}
}
}
// Unlock the document in the RDT if necessary
if(docInRdt && rdt != null)
{
ErrorHandler.ThrowOnFailure(rdt.UnlockDocument((uint)(_VSRDTFLAGS.RDT_ReadLock | _VSRDTFLAGS.RDT_Unlock_NoSave), cookie));
}
if(ErrorHandler.Failed(hr))
{
// If this failed then it's probably not a text file. In that case,
// we just read the file as a binary
bufferContents = File.ReadAllText(fileName);
}
}
return bufferContents;
}
/// <summary>
/// Returns TRUE if open and dirty. Note that documents can be open without a
/// window frame so be careful. Returns the DocData and doc cookie if requested
/// </summary>
/// <param name="document">document path</param>
/// <param name="pHier">hierarchy</param>
/// <param name="ppDocData">doc data associated with document</param>
/// <param name="cookie">item cookie</param>
/// <returns>True if FIle is dirty</returns>
private bool VerifyFileDirtyInRdt(string document, out IVsHierarchy pHier, out IVsPersistDocData ppDocData, out uint cookie)
{
int ret = 0;
pHier = null;
ppDocData = null;
cookie = 0;
IVsRunningDocumentTable rdt = this.projectMgr.GetService(typeof(IVsRunningDocumentTable)) as IVsRunningDocumentTable;
if(rdt != null)
{
IntPtr docData;
uint dwCookie = 0;
IVsHierarchy srpHier;
uint itemid = VSConstants.VSITEMID_NIL;
ErrorHandler.ThrowOnFailure(rdt.FindAndLockDocument((uint)_VSRDTFLAGS.RDT_NoLock, document, out srpHier, out itemid, out docData, out dwCookie));
IVsPersistHierarchyItem srpIVsPersistHierarchyItem = srpHier as IVsPersistHierarchyItem;
if(srpIVsPersistHierarchyItem != null)
{
// Found in the RDT. See if it is dirty
try
{
ErrorHandler.ThrowOnFailure(srpIVsPersistHierarchyItem.IsItemDirty(itemid, docData, out ret));
cookie = dwCookie;
ppDocData = Marshal.GetObjectForIUnknown(docData) as IVsPersistDocData;
}
finally
{
if(docData != IntPtr.Zero)
{
Marshal.Release(docData);
}
pHier = srpHier;
}
}
}
return (ret == 1);
}
#endregion
#region QueryEditQuerySave helpers
/// <summary>
/// This function asks to the QueryEditQuerySave service if it is possible to
/// edit the file.
/// </summary>
private bool CanEditFile(string documentMoniker)
{
Trace.WriteLine(string.Format(CultureInfo.CurrentCulture, "\t**** CanEditFile called ****"));
// Check the status of the recursion guard
if(this.gettingCheckoutStatus)
{
return false;
}
try
{
// Set the recursion guard
this.gettingCheckoutStatus = true;
// Get the QueryEditQuerySave service
IVsQueryEditQuerySave2 queryEditQuerySave = (IVsQueryEditQuerySave2)this.projectMgr.GetService(typeof(SVsQueryEditQuerySave));
// Now call the QueryEdit method to find the edit status of this file
string[] documents = { documentMoniker };
uint result;
uint outFlags;
// Note that this function can popup a dialog to ask the user to checkout the file.
// When this dialog is visible, it is possible to receive other request to change
// the file and this is the reason for the recursion guard.
int hr = queryEditQuerySave.QueryEditFiles(
0, // Flags
1, // Number of elements in the array
documents, // Files to edit
null, // Input flags
null, // Input array of VSQEQS_FILE_ATTRIBUTE_DATA
out result, // result of the checkout
out outFlags // Additional flags
);
if(ErrorHandler.Succeeded(hr) && (result == (uint)tagVSQueryEditResult.QER_EditOK))
{
// In this case (and only in this case) we can return true from this function.
return true;
}
}
finally
{
this.gettingCheckoutStatus = false;
}
return false;
}
#endregion
}
}