/*************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. This code is licensed under the Visual Studio SDK license terms. THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR 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 { /// /// Provides support for single file generator. /// internal class SingleFileGenerator : ISingleFileGenerator, IVsGeneratorProgress { #region fields private bool gettingCheckoutStatus; private bool runningGenerator; private ProjectNode projectMgr; #endregion #region ctors /// /// Overloadde ctor. /// /// The associated project 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 /// /// Runs the generator on the current project item. /// /// /// 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 /// /// Invokes the specified generator /// /// The node on which to invoke the generator. 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; } } /// /// 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. /// /// Returns the computed name space 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; } /// /// This is called after the single file generator has been invoked to create or update the code file. /// /// The node associated to the generator /// data to update the file with /// size of the data /// Name of the file to update or create /// full path of the file 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 /// /// Returns the buffer contents for a moniker. /// /// Buffer contents 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; } /// /// 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 /// /// document path /// hierarchy /// doc data associated with document /// item cookie /// True if FIle is dirty 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 /// /// This function asks to the QueryEditQuerySave service if it is possible to /// edit the file. /// 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 } }