/// <summary> /// Renames the file in the hierarchy by removing old node and adding a new node in the hierarchy. /// </summary> /// <param name="oldFileName">The old file name.</param> /// <param name="newFileName">The new file name</param> /// <param name="newParentId">The new parent id of the item.</param> /// <returns>The newly added FileNode.</returns> /// <remarks>While a new node will be used to represent the item, the underlying MSBuild item will be the same and as a result file properties saved in the project file will not be lost.</remarks> public virtual FileNode RenameFileNode(string oldFileName, string newFileName, uint newParentId) { if (string.Compare(oldFileName, newFileName, StringComparison.Ordinal) == 0) { // We do not want to rename the same file return(null); } string[] file = new string[1]; file[0] = newFileName; VSADDRESULT[] result = new VSADDRESULT[1]; Guid emptyGuid = Guid.Empty; FileNode childAdded = null; string originalInclude = this.ItemNode.Item.UnevaluatedInclude; return(Transactional.Try( // Action () => { // It's unfortunate that MPF implements rename in terms of AddItemWithSpecific. Since this // is the case, we have to pass false to prevent AddITemWithSpecific to fire Add events on // the IVsTrackProjectDocuments2 tracker. Otherwise, clients listening to this event // (SCCI, for example) will be really confused. using (this.ProjectMgr.ExtensibilityEventsHelper.SuspendEvents()) { var currentId = this.ID; // actual deletion is delayed until all checks are passed Func <uint> getIdOfExistingItem = () => { this.OnItemDeleted(); this.Parent.RemoveChild(this); return currentId; }; // raises OnItemAdded inside ErrorHandler.ThrowOnFailure(this.ProjectMgr.AddItemWithSpecific(newParentId, VSADDITEMOPERATION.VSADDITEMOP_OPENFILE, null, 0, file, IntPtr.Zero, 0, ref emptyGuid, null, ref emptyGuid, result, false, getIdOfExistingItem)); childAdded = this.ProjectMgr.FindChild(newFileName) as FileNode; Debug.Assert(childAdded != null, "Could not find the renamed item in the hierarchy"); } }, // Compensation () => { // it failed, but 'this' is dead and 'childAdded' is here to stay, so fix the latter back using (this.ProjectMgr.ExtensibilityEventsHelper.SuspendEvents()) { childAdded.ItemNode.Rename(originalInclude); childAdded.ItemNode.RefreshProperties(); } }, // Continuation () => { // Since this node has been removed all of its state is zombied at this point // Do not call virtual methods after this point since the object is in a deleted state. // Remove the item created by the add item. We need to do this otherwise we will have two items. // Please be aware that we have not removed the ItemNode associated to the removed file node from the hierrachy. // What we want to achieve here is to reuse the existing build item. // We want to link to the newly created node to the existing item node and addd the new include. //temporarily keep properties from new itemnode since we are going to overwrite it string newInclude = childAdded.ItemNode.Item.UnevaluatedInclude; string dependentOf = childAdded.ItemNode.GetMetadata(ProjectFileConstants.DependentUpon); childAdded.ItemNode.RemoveFromProjectFile(); // Assign existing msbuild item to the new childnode childAdded.ItemNode = this.ItemNode; childAdded.ItemNode.Item.ItemType = this.ItemNode.ItemName; childAdded.ItemNode.Item.Xml.Include = newInclude; if (!string.IsNullOrEmpty(dependentOf)) { childAdded.ItemNode.SetMetadata(ProjectFileConstants.DependentUpon, dependentOf); } childAdded.ItemNode.RefreshProperties(); // Extensibilty events has rename this.ProjectMgr.ExtensibilityEventsHelper.FireItemRenamed(childAdded, Path.GetFileName(originalInclude)); //Update the new document in the RDT. try { DocumentManager.RenameDocument(this.ProjectMgr.Site, oldFileName, newFileName, childAdded.ID); // The current automation node is renamed, but the hierarchy ID is now pointing to the old item, which // is invalid. Update it. this.ID = childAdded.ID; //Update FirstChild childAdded.FirstChild = this.FirstChild; //Update ChildNodes SetNewParentOnChildNodes(childAdded); RenameChildNodes(childAdded); return childAdded; } finally { //Select the new node in the hierarchy IVsUIHierarchyWindow uiWindow = UIHierarchyUtilities.GetUIHierarchyWindow(this.ProjectMgr.Site, SolutionExplorer); uiWindow.ExpandItem(this.ProjectMgr.InteropSafeIVsUIHierarchy, childAdded.ID, EXPANDFLAGS.EXPF_SelectItem); } })); }
/// <summary> /// Get's called to rename the eventually running document this hierarchyitem points to /// </summary> /// returns FALSE if the doc can not be renamed public bool RenameDocument(string oldName, string newName) { IVsRunningDocumentTable pRDT = this.GetService(typeof(IVsRunningDocumentTable)) as IVsRunningDocumentTable; if (pRDT == null) { return(false); } IntPtr docData = IntPtr.Zero; IVsHierarchy pIVsHierarchy; uint itemId; uint uiVsDocCookie; SuspendFileChanges sfc = new SuspendFileChanges(this.ProjectMgr.Site, oldName); sfc.Suspend(); try { VSRENAMEFILEFLAGS renameflag = VSRENAMEFILEFLAGS.VSRENAMEFILEFLAGS_NoFlags; ErrorHandler.ThrowOnFailure(pRDT.FindAndLockDocument((uint)_VSRDTFLAGS.RDT_NoLock, oldName, out pIVsHierarchy, out itemId, out docData, out uiVsDocCookie)); if (pIVsHierarchy != null && !Utilities.IsSameComObject(pIVsHierarchy, this.ProjectMgr)) { // Don't rename it if it wasn't opened by us. return(false); } // ask other potentially running packages if (!this.ProjectMgr.Tracker.CanRenameItem(oldName, newName, renameflag)) { return(false); } // Allow the user to "fix" the project by renaming the item in the hierarchy // to the real name of the file on disk. bool shouldRenameInStorage = IsFileOnDisk(oldName) || !IsFileOnDisk(newName); Transactional.Try( // Action () => { if (shouldRenameInStorage) { RenameInStorage(oldName, newName); } }, // Compensation () => { if (shouldRenameInStorage) { RenameInStorage(newName, oldName); } }, // Continuation () => { string newFileName = Path.GetFileName(newName); string oldCaption = this.Caption; Transactional.Try( // Action () => DocumentManager.UpdateCaption(this.ProjectMgr.Site, newFileName, docData), // Compensation () => DocumentManager.UpdateCaption(this.ProjectMgr.Site, oldCaption, docData), // Continuation () => { bool caseOnlyChange = NativeMethods.IsSamePath(oldName, newName); if (!caseOnlyChange) { // Check out the project file if necessary. if (!this.ProjectMgr.QueryEditProjectFile(false)) { throw Marshal.GetExceptionForHR(VSConstants.OLE_E_PROMPTSAVECANCELLED); } this.RenameFileNode(oldName, newName); } else { this.RenameCaseOnlyChange(newFileName); } bool extensionWasChanged = (0 != String.Compare(Path.GetExtension(oldName), Path.GetExtension(newName), StringComparison.OrdinalIgnoreCase)); if (extensionWasChanged) { // Update the BuildAction this.ItemNode.ItemName = this.ProjectMgr.DefaultBuildAction(newName); } this.ProjectMgr.Tracker.OnItemRenamed(oldName, newName, renameflag); }); }); } finally { if (docData != IntPtr.Zero) { Marshal.Release(docData); } sfc.Resume(); // can throw, e.g. when RenameFileNode failed, but file was renamed on disk and now editor cannot find file } return(true); }
/// <summary> /// Performs a SaveAs operation of an open document. Called from SaveItem after the running document table has been updated with the new doc data. /// </summary> /// <param name="docData">A pointer to the document in the rdt</param> /// <param name="newFilePath">The new file path to the document</param> /// <returns></returns> public override int AfterSaveItemAs(IntPtr docData, string newFilePath) { if (String.IsNullOrEmpty(newFilePath)) { throw new ArgumentException(SR.GetString(SR.ParameterCannotBeNullOrEmpty, CultureInfo.CurrentUICulture), "newFilePath"); } int returnCode = VSConstants.S_OK; newFilePath = newFilePath.Trim(); //Identify if Path or FileName are the same for old and new file string newDirectoryName = Path.GetDirectoryName(newFilePath); Uri newDirectoryUri = new Uri(newDirectoryName); string newCanonicalDirectoryName = newDirectoryUri.LocalPath; newCanonicalDirectoryName = newCanonicalDirectoryName.TrimEnd(Path.DirectorySeparatorChar); string oldCanonicalDirectoryName = new Uri(Path.GetDirectoryName(this.GetMkDocument())).LocalPath; oldCanonicalDirectoryName = oldCanonicalDirectoryName.TrimEnd(Path.DirectorySeparatorChar); string errorMessage = String.Empty; bool isSamePath = NativeMethods.IsSamePath(newCanonicalDirectoryName, oldCanonicalDirectoryName); bool isSameFile = NativeMethods.IsSamePath(newFilePath, this.Url); // Currently we do not support if the new directory is located outside the project cone string projectCannonicalDirecoryName = new Uri(this.ProjectMgr.ProjectFolder).LocalPath; projectCannonicalDirecoryName = projectCannonicalDirecoryName.TrimEnd(Path.DirectorySeparatorChar); if (!isSamePath && newCanonicalDirectoryName.IndexOf(projectCannonicalDirecoryName, StringComparison.OrdinalIgnoreCase) == -1) { errorMessage = String.Format(CultureInfo.CurrentCulture, SR.GetString(SR.LinkedItemsAreNotSupported, CultureInfo.CurrentUICulture), Path.GetFileNameWithoutExtension(newFilePath)); throw new InvalidOperationException(errorMessage); } //Get target container HierarchyNode targetContainer = null; if (isSamePath) { targetContainer = this.Parent; } else if (NativeMethods.IsSamePath(newCanonicalDirectoryName, projectCannonicalDirecoryName)) { //the projectnode is the target container targetContainer = this.ProjectMgr; } else { //search for the target container among existing child nodes targetContainer = this.ProjectMgr.FindChild(newDirectoryName); if (targetContainer != null && (targetContainer is FileNode)) { // We already have a file node with this name in the hierarchy. errorMessage = String.Format(CultureInfo.CurrentCulture, SR.GetString(SR.FileAlreadyExistsAndCannotBeRenamed, CultureInfo.CurrentUICulture), Path.GetFileNameWithoutExtension(newFilePath)); throw new InvalidOperationException(errorMessage); } } if (targetContainer == null) { // Add a chain of subdirectories to the project. string relativeUri = PackageUtilities.GetPathDistance(this.ProjectMgr.BaseURI.Uri, newDirectoryUri); Debug.Assert(!String.IsNullOrEmpty(relativeUri) && relativeUri != newDirectoryUri.LocalPath, "Could not make pat distance of " + this.ProjectMgr.BaseURI.Uri.LocalPath + " and " + newDirectoryUri); targetContainer = this.ProjectMgr.CreateFolderNodes(relativeUri); } Debug.Assert(targetContainer != null, "We should have found a target node by now"); //Suspend file changes while we rename the document string oldrelPath = this.ItemNode.GetMetadata(ProjectFileConstants.Include); string oldName = Path.Combine(this.ProjectMgr.ProjectFolder, oldrelPath); SuspendFileChanges sfc = new SuspendFileChanges(this.ProjectMgr.Site, oldName); sfc.Suspend(); try { // Rename the node. DocumentManager.UpdateCaption(this.ProjectMgr.Site, Path.GetFileName(newFilePath), docData); // Check if the file name was actually changed. // In same cases (e.g. if the item is a file and the user has changed its encoding) this function // is called even if there is no real rename. var oldParent = this.Parent; if (!isSameFile || (oldParent.ID != targetContainer.ID)) { // The path of the file is changed or its parent is changed; in both cases we have // to rename the item. this.RenameFileNode(oldName, newFilePath, targetContainer.ID); OnInvalidateItems(oldParent); // This is what othe project systems do; for the purposes of source control, the old file is removed, and a new file is added // (althought the old file stays on disk!) this.ProjectMgr.Tracker.OnItemRemoved(oldName, VSREMOVEFILEFLAGS.VSREMOVEFILEFLAGS_NoFlags); this.ProjectMgr.Tracker.OnItemAdded(newFilePath, VSADDFILEFLAGS.VSADDFILEFLAGS_NoFlags); } } catch (Exception e) { Trace.WriteLine("Exception : " + e.Message); this.RecoverFromRenameFailure(newFilePath, oldrelPath); throw; } finally { sfc.Resume(); } return(returnCode); }