diff --git a/vsintegration/src/FSharp.ProjectSystem.Base/Project/AssemblyReferenceNode.cs b/vsintegration/src/FSharp.ProjectSystem.Base/Project/AssemblyReferenceNode.cs index 8dc2a6f71d98b14bbae1687cfae591a4ffed12b6..78958291985d9d99dd403372d01c8be56474f262 100644 --- a/vsintegration/src/FSharp.ProjectSystem.Base/Project/AssemblyReferenceNode.cs +++ b/vsintegration/src/FSharp.ProjectSystem.Base/Project/AssemblyReferenceNode.cs @@ -394,7 +394,7 @@ public override bool CanDeleteItem(__VSDELETEITEMOPERATION deleteOperation) return base.CanDeleteItem(deleteOperation); } - public override void Remove(bool removeFromStorage) + public override void Remove(bool removeFromStorage, bool promptSave = true) { // AssemblyReference doesn't backed by the document - its removal is simply modification of the project file // we disable IVsTrackProjectDocuments2 events to avoid confusing messages from SCC @@ -403,7 +403,7 @@ public override void Remove(bool removeFromStorage) { ProjectMgr.EventTriggeringFlag = oldFlag | ProjectNode.EventTriggering.DoNotTriggerTrackerEvents; - base.Remove(removeFromStorage); + base.Remove(removeFromStorage, promptSave); // invoke ComputeSourcesAndFlags to refresh compiler flags // it was the only useful thing performed by one of IVsTrackProjectDocuments2 listeners diff --git a/vsintegration/src/FSharp.ProjectSystem.Base/Project/Automation/OAProjectItem.cs b/vsintegration/src/FSharp.ProjectSystem.Base/Project/Automation/OAProjectItem.cs index 146fd45f1cfd4a1b7f12e9503a701a847520820d..70f1a236498eeb92f5eec5059f322e97dad69e12 100644 --- a/vsintegration/src/FSharp.ProjectSystem.Base/Project/Automation/OAProjectItem.cs +++ b/vsintegration/src/FSharp.ProjectSystem.Base/Project/Automation/OAProjectItem.cs @@ -350,7 +350,7 @@ public virtual void Remove() extensibility.EnterAutomationFunction(); try { - this.node.Remove(false); + this.node.Remove(removeFromStorage: false); } finally { @@ -381,7 +381,7 @@ public virtual void Delete() try { - this.node.Remove(true); + this.node.Remove(removeFromStorage: true, promptSave: false); } finally { diff --git a/vsintegration/src/FSharp.ProjectSystem.Base/Project/Automation/VSProject/OAReferenceBase.cs b/vsintegration/src/FSharp.ProjectSystem.Base/Project/Automation/VSProject/OAReferenceBase.cs index 4de53d9e1208c0d52675183264376092fe9e3408..54a74fc674849dcb1db3ebef682edde3a7e009cf 100644 --- a/vsintegration/src/FSharp.ProjectSystem.Base/Project/Automation/VSProject/OAReferenceBase.cs +++ b/vsintegration/src/FSharp.ProjectSystem.Base/Project/Automation/VSProject/OAReferenceBase.cs @@ -158,7 +158,7 @@ public virtual string PublicKeyToken public virtual void Remove() { UIThread.DoOnUIThread(delegate(){ - BaseReferenceNode.Remove(false); + BaseReferenceNode.Remove(removeFromStorage: false); }); } diff --git a/vsintegration/src/FSharp.ProjectSystem.Base/Project/FileNode.cs b/vsintegration/src/FSharp.ProjectSystem.Base/Project/FileNode.cs index 9d96ea90cde3c7b8be7b9abf0c5d41e78440072c..a2bb11f49dc88ddfa31bbe952a62e97261bd16cb 100644 --- a/vsintegration/src/FSharp.ProjectSystem.Base/Project/FileNode.cs +++ b/vsintegration/src/FSharp.ProjectSystem.Base/Project/FileNode.cs @@ -210,6 +210,14 @@ internal FileNode(ProjectNode root, ProjectElement element, uint? hierarchyId = } } + public virtual string RelativeFilePath + { + get + { + return PackageUtilities.MakeRelativeIfRooted(this.Url, this.ProjectMgr.BaseURI); + } + } + public override NodeProperties CreatePropertiesObject() { return new FileNodeProperties(this); diff --git a/vsintegration/src/FSharp.ProjectSystem.Base/Project/FolderNode.cs b/vsintegration/src/FSharp.ProjectSystem.Base/Project/FolderNode.cs index 1cb62b69ac02cc3b57f7031020a38ba4edacf838..a305a5cf03ccf0cb6c25eb1364e9c65fb65a2cca 100644 --- a/vsintegration/src/FSharp.ProjectSystem.Base/Project/FolderNode.cs +++ b/vsintegration/src/FSharp.ProjectSystem.Base/Project/FolderNode.cs @@ -18,6 +18,7 @@ using VsCommands2K = Microsoft.VisualStudio.VSConstants.VSStd2KCmdID; using System.Diagnostics.CodeAnalysis; using System.Collections.Generic; +using System.Linq; namespace Microsoft.VisualStudio.FSharp.ProjectSystem { @@ -93,21 +94,15 @@ public override int SetEditLabel(string label) string newPath = Path.Combine(new DirectoryInfo(this.Url).Parent.FullName, label); - // Verify that No Directory/file already exists with the new name among current children + // Verify that No Directory/file already exists with the new name among siblings for (HierarchyNode n = Parent.FirstChild; n != null; n = n.NextSibling) { if (n != this && String.Compare(n.Caption, label, StringComparison.OrdinalIgnoreCase) == 0) { - return ShowFileOrFolderAlreadExistsErrorMessage(newPath); + return ShowErrorMessage(SR.FileOrFolderAlreadyExists, newPath); } } - // Verify that No Directory/file already exists with the new name on disk - if (Directory.Exists(newPath) || FSLib.Shim.FileSystem.SafeExists(newPath)) - { - return ShowFileOrFolderAlreadExistsErrorMessage(newPath); - } - try { RenameFolder(label); @@ -380,7 +375,8 @@ public virtual void RenameDirectory(string newPath) { if (Directory.Exists(newPath)) { - ShowFileOrFolderAlreadExistsErrorMessage(newPath); + ShowErrorMessage(SR.FileOrFolderAlreadyExists, newPath); + return; } Directory.Move(this.Url, newPath); @@ -389,12 +385,34 @@ public virtual void RenameDirectory(string newPath) private void RenameFolder(string newName) { - // Do the rename (note that we only do the physical rename if the leaf name changed) string newPath = Path.Combine(this.Parent.VirtualNodeName, newName); + string newFullPath = Path.Combine(this.ProjectMgr.ProjectFolder, newPath); + + // Only do the physical rename if the leaf name changed if (String.Compare(Path.GetFileName(VirtualNodeName), newName, StringComparison.Ordinal) != 0) { - this.RenameDirectory(Path.Combine(this.ProjectMgr.ProjectFolder, newPath)); + // Verify that no directory/file already exists with the new name on disk. + // If it does, just subsume that name if our directory is empty. + if (Directory.Exists(newFullPath) || FSLib.Shim.FileSystem.SafeExists(newFullPath)) + { + // We can't delete our old directory as it is not empty + if (Directory.EnumerateFileSystemEntries(this.Url).Any()) + { + ShowErrorMessage(SR.FolderCannotBeRenamed, newPath); + return; + } + + // Try to delete the old (empty) directory. + // Note that we don't want to delete recursively in case a file was added between + // when we checked and when we went to delete (potential race condition). + Directory.Delete(this.Url, false); + } + else + { + this.RenameDirectory(newFullPath); + } } + this.VirtualNodeName = newPath; this.ItemNode.Rename(VirtualNodeName); @@ -422,13 +440,15 @@ private void RenameFolder(string newName) /// /// Show error message if not in automation mode, otherwise throw exception /// - /// path of file or folder already existing on disk + /// Parameter for resource string format /// S_OK - private int ShowFileOrFolderAlreadExistsErrorMessage(string newPath) + private int ShowErrorMessage(string resourceName, string parameter) { - //A file or folder with the name '{0}' already exists on disk at this location. Please choose another name. - //If this file or folder does not appear in the Solution Explorer, then it is not currently part of your project. To view files which exist on disk, but are not in the project, select Show All Files from the Project menu. - string errorMessage = (String.Format(CultureInfo.CurrentCulture, SR.GetString(SR.FileOrFolderAlreadyExists, CultureInfo.CurrentUICulture), newPath)); + // Most likely the cause of: + // A file or folder with the name '{0}' already exists on disk at this location. Please choose another name. + // -or- + // This folder cannot be renamed to '{0}' as it already exists on disk. + string errorMessage = String.Format(CultureInfo.CurrentCulture, SR.GetStringWithCR(resourceName), parameter); if (!Utilities.IsInAutomationFunction(this.ProjectMgr.Site)) { string title = null; diff --git a/vsintegration/src/FSharp.ProjectSystem.Base/Project/HierarchyNode.cs b/vsintegration/src/FSharp.ProjectSystem.Base/Project/HierarchyNode.cs index 9620fd78fd15fb47734e98f00058e3c52532440f..a1c5339198b0ea3ef290622e272fe6868e6ac99e 100644 --- a/vsintegration/src/FSharp.ProjectSystem.Base/Project/HierarchyNode.cs +++ b/vsintegration/src/FSharp.ProjectSystem.Base/Project/HierarchyNode.cs @@ -524,8 +524,9 @@ public virtual void AddChild(HierarchyNode node) Object nodeWithSameID = this.projectMgr.ItemIdMap[node.hierarchyId]; if (!Object.ReferenceEquals(node, nodeWithSameID as HierarchyNode)) { - if (nodeWithSameID == null && node.ID <= this.ProjectMgr.ItemIdMap.Count) - { // reuse our hierarchy id if possible. + // reuse our hierarchy id if possible. + if (nodeWithSameID == null) + { this.projectMgr.ItemIdMap.SetAt(node.hierarchyId, this); } else @@ -928,7 +929,7 @@ public virtual string GetMkDocument() /// Removes items from the hierarchy. Project overwrites this /// /// - public virtual void Remove(bool removeFromStorage) + public virtual void Remove(bool removeFromStorage, bool promptSave = true) { string documentToRemove = this.GetMkDocument(); @@ -944,7 +945,7 @@ public virtual void Remove(bool removeFromStorage) DocumentManager manager = this.GetDocumentManager(); if (manager != null) { - if (manager.Close(!removeFromStorage ? __FRAMECLOSE.FRAMECLOSE_PromptSave : __FRAMECLOSE.FRAMECLOSE_NoSave) == VSConstants.E_ABORT) + if (manager.Close(promptSave ? __FRAMECLOSE.FRAMECLOSE_PromptSave : __FRAMECLOSE.FRAMECLOSE_NoSave) == VSConstants.E_ABORT) { // User cancelled operation in message box. return; @@ -963,7 +964,7 @@ public virtual void Remove(bool removeFromStorage) // Remove child if any before removing from the hierarchy for (HierarchyNode child = this.FirstChild; child != null; child = child.NextSibling) { - child.Remove(removeFromStorage); + child.Remove(removeFromStorage: false, promptSave: promptSave); } HierarchyNode thisParentNode = this.parentNode; @@ -1114,7 +1115,7 @@ public virtual HierarchyNode GetDragTargetHandlerNode() /// Add a new Folder to the project hierarchy. /// /// S_OK if succeeded, otherwise an error - public virtual int AddNewFolder() + public virtual int AddNewFolder(Action moveNode=null) { // Check out the project file. if (!this.ProjectMgr.QueryEditProjectFile(false)) @@ -1129,20 +1130,25 @@ public virtual int AddNewFolder() ErrorHandler.ThrowOnFailure(this.projectMgr.GenerateUniqueItemName(this.hierarchyId, String.Empty, String.Empty, out newFolderName)); // create the project part of it, the project file - HierarchyNode child = this.ProjectMgr.CreateFolderNodes(Path.Combine(this.virtualNodeName, newFolderName)); + HierarchyNode node = this.ProjectMgr.CreateFolderNodes(Path.Combine(this.virtualNodeName, newFolderName)); - if (child is FolderNode) + if (node is FolderNode) { - ((FolderNode)child).CreateDirectory(); + ((FolderNode)node).CreateDirectory(); } + if (moveNode != null) + { + moveNode(node); + } + // If we are in automation mode then skip the ui part which is about renaming the folder if (!Utilities.IsInAutomationFunction(this.projectMgr.Site)) { IVsUIHierarchyWindow uiWindow = UIHierarchyUtilities.GetUIHierarchyWindow(this.projectMgr.Site, SolutionExplorer); // we need to get into label edit mode now... // so first select the new guy... - ErrorHandler.ThrowOnFailure(uiWindow.ExpandItem(this.projectMgr.InteropSafeIVsUIHierarchy, child.hierarchyId, EXPANDFLAGS.EXPF_SelectItem)); + ErrorHandler.ThrowOnFailure(uiWindow.ExpandItem(this.projectMgr.InteropSafeIVsUIHierarchy, node.hierarchyId, EXPANDFLAGS.EXPF_SelectItem)); // them post the rename command to the shell. Folder verification and creation will // happen in the setlabel code... IVsUIShell shell = this.projectMgr.Site.GetService(typeof(SVsUIShell)) as IVsUIShell; @@ -1214,7 +1220,7 @@ public virtual void DoDefaultAction() public virtual int ExcludeFromProject() { Debug.Assert(this.ProjectMgr != null, "The project item " + this.ToString() + " has not been initialised correctly. It has a null ProjectMgr"); - this.Remove(false); + this.Remove(removeFromStorage: false); return VSConstants.S_OK; } @@ -1403,14 +1409,19 @@ public virtual int ExecCommandOnNode(Guid cmdGroup, uint cmd, uint nCmdexecopt, } else if (cmdGroup == VsMenus.guidStandardCommandSet97) { + int result = -1; HierarchyNode nodeToAddTo = this.GetDragTargetHandlerNode(); switch ((VsCommands)cmd) { case VsCommands.AddNewItem: - return nodeToAddTo.AddItemToHierarchy(HierarchyAddType.AddNewItem); + result = nodeToAddTo.AddItemToHierarchy(HierarchyAddType.AddNewItem); + this.projectMgr.EnsureMSBuildAndSolutionExplorerAreInSync(); + return result; case VsCommands.AddExistingItem: - return nodeToAddTo.AddItemToHierarchy(HierarchyAddType.AddExistingItem); + result = nodeToAddTo.AddItemToHierarchy(HierarchyAddType.AddExistingItem); + this.projectMgr.EnsureMSBuildAndSolutionExplorerAreInSync(); + return result; case VsCommands.NewFolder: return nodeToAddTo.AddNewFolder(); @@ -2876,7 +2887,8 @@ public virtual int DeleteItem(uint delItemOp, uint itemId) HierarchyNode node = this.projectMgr.NodeFromItemId(itemId); if (node != null) { - node.Remove((delItemOp & (uint)__VSDELETEITEMOPERATION.DELITEMOP_DeleteFromStorage) != 0); + var removeFromStorage = (delItemOp & (uint)__VSDELETEITEMOPERATION.DELITEMOP_DeleteFromStorage) != 0; + node.Remove(removeFromStorage, promptSave: !removeFromStorage); return VSConstants.S_OK; } @@ -3294,5 +3306,38 @@ public int GetResourceItem(uint itemidDocument, string pszCulture, uint grfPRF, } public virtual __VSPROVISIONALVIEWINGSTATUS ProvisionalViewingStatus => __VSPROVISIONALVIEWINGSTATUS.PVS_Disabled; + + /// + /// All nodes that are direct children of this node. + /// + public virtual IEnumerable AllChildren + { + get + { + for (var child = this.FirstChild; child != null; child = child.NextSibling) + { + yield return child; + } + } + } + + /// + /// All nodes that are my children, plus their children, ad infinitum. + /// + public virtual IEnumerable AllDescendants + { + get + { + foreach (var child in this.AllChildren) + { + yield return child; + + foreach (var descendant in child.AllDescendants) + { + yield return descendant; + } + } + } + } } } diff --git a/vsintegration/src/FSharp.ProjectSystem.Base/Project/LinkedFileNode.cs b/vsintegration/src/FSharp.ProjectSystem.Base/Project/LinkedFileNode.cs index 0c2c54bfab30a7d91c66c0ab594da0f96fd7c185..6e0c5225b513caa41e40bf8f6127581c99e647f5 100644 --- a/vsintegration/src/FSharp.ProjectSystem.Base/Project/LinkedFileNode.cs +++ b/vsintegration/src/FSharp.ProjectSystem.Base/Project/LinkedFileNode.cs @@ -158,6 +158,19 @@ public bool IsImported public override int MenuCommandId { get { return VsMenus.IDM_VS_CTXT_ITEMNODE; } - } + } + + public override string RelativeFilePath + { + get + { + string link = this.ItemNode.GetMetadata(ProjectFileConstants.Link); + if (string.IsNullOrEmpty(link)) + { + return base.RelativeFilePath; + } + return link; + } + } } } diff --git a/vsintegration/src/FSharp.ProjectSystem.Base/Project/Microsoft.VisualStudio.Package.Project.cs b/vsintegration/src/FSharp.ProjectSystem.Base/Project/Microsoft.VisualStudio.Package.Project.cs index 2a568dd8a374f6e618585753666396bd1e0b0852..168bc97271ed731822a08fc20ede31ebd8d4c7bc 100644 --- a/vsintegration/src/FSharp.ProjectSystem.Base/Project/Microsoft.VisualStudio.Package.Project.cs +++ b/vsintegration/src/FSharp.ProjectSystem.Base/Project/Microsoft.VisualStudio.Package.Project.cs @@ -115,6 +115,7 @@ public sealed class SR public const string FileName = "FileName"; public const string FileNameDescription = "FileNameDescription"; public const string FileOrFolderAlreadyExists = "FileOrFolderAlreadyExists"; + public const string FolderCannotBeRenamed = "FolderCannotBeRenamed"; public const string FileOrFolderCannotBeFound = "FileOrFolderCannotBeFound"; public const string FileProperties = "FileProperties"; public const string FolderName = "FolderName"; diff --git a/vsintegration/src/FSharp.ProjectSystem.Base/Project/Microsoft.VisualStudio.Package.Project.resx b/vsintegration/src/FSharp.ProjectSystem.Base/Project/Microsoft.VisualStudio.Package.Project.resx index 2efe57568f940df8894bf7cb5a30832248de85cd..cfca80897c079cae1390bc91ad629ad896875437 100644 --- a/vsintegration/src/FSharp.ProjectSystem.Base/Project/Microsoft.VisualStudio.Package.Project.resx +++ b/vsintegration/src/FSharp.ProjectSystem.Base/Project/Microsoft.VisualStudio.Package.Project.resx @@ -407,6 +407,9 @@ A file or folder with the name '{0}' already exists on disk at this location. Please choose another name. + + This folder cannot be renamed to '{0}' as it already exists on disk.\n\nOnly empty folders can be renamed to existing folders. This folder contains files within it on disk. + Build diff --git a/vsintegration/src/FSharp.ProjectSystem.Base/Project/ProjectNode.CopyPaste.cs b/vsintegration/src/FSharp.ProjectSystem.Base/Project/ProjectNode.CopyPaste.cs index 07675232287ca3c8e7a90a34a3b929f0b2c3d945..c4df248c9d26e528825660641a9e8078cc693f3a 100644 --- a/vsintegration/src/FSharp.ProjectSystem.Base/Project/ProjectNode.CopyPaste.cs +++ b/vsintegration/src/FSharp.ProjectSystem.Base/Project/ProjectNode.CopyPaste.cs @@ -555,7 +555,7 @@ public virtual void WalkSourceProjectAndAdd(IVsHierarchy sourceHierarchy, uint i while (currentItemID != VSConstants.VSITEMID_NIL) { variant = null; - ErrorHandler.ThrowOnFailure(sourceHierarchy.GetProperty(itemId, (int)__VSHPROPID.VSHPROPID_NextVisibleSibling, out variant)); + ErrorHandler.ThrowOnFailure(sourceHierarchy.GetProperty(currentItemID, (int)__VSHPROPID.VSHPROPID_NextVisibleSibling, out variant)); currentItemID = (uint)(int)variant; WalkSourceProjectAndAdd(sourceHierarchy, currentItemID, targetNode, true); } @@ -974,7 +974,7 @@ public void CleanupSelectionDataObject(bool dropped, bool cut, bool moved, bool } } - node.Remove(true); + node.Remove(removeFromStorage: true, promptSave: false); } else if (w != null) { diff --git a/vsintegration/src/FSharp.ProjectSystem.Base/Project/ProjectNode.cs b/vsintegration/src/FSharp.ProjectSystem.Base/Project/ProjectNode.cs index 69ed1fbe32026001ec73d1a72c2d400380f5c3ec..20f0d67b815791cfec5ed182fbaccab9981cf9b7 100644 --- a/vsintegration/src/FSharp.ProjectSystem.Base/Project/ProjectNode.cs +++ b/vsintegration/src/FSharp.ProjectSystem.Base/Project/ProjectNode.cs @@ -1521,7 +1521,7 @@ public override int SetGuidProperty(int propid, ref Guid guid) /// Removes items from the hierarchy. /// /// Project overwrites this. - public override void Remove(bool removeFromStorage) + public override void Remove(bool removeFromStorage, bool promptSave = true) { // the project will not be deleted from disk, just removed if (removeFromStorage) @@ -1529,6 +1529,8 @@ public override void Remove(bool removeFromStorage) return; } + Debug.Assert(promptSave, "Non-save prompting removal is not supported"); + // Remove the entire project from the solution IVsSolution solution = this.Site.GetService(typeof(SVsSolution)) as IVsSolution; uint iOption = 1; // SLNSAVEOPT_PromptSave @@ -3642,7 +3644,7 @@ internal virtual ProjectElement AddFileToMsBuild(string file) { ProjectElement newItem; - string itemPath = PackageUtilities.MakeRelativeIfRooted(file, this.BaseURI); + string itemPath = PackageUtilities.MakeRelative(this.BaseURI.AbsoluteUrl, file); Debug.Assert(!Path.IsPathRooted(itemPath), "Cannot add item with full path."); string defaultBuildAction = this.DefaultBuildAction(itemPath); @@ -4830,7 +4832,7 @@ public virtual int GetMkDocument(uint itemId, out string mkDoc) return VSConstants.S_OK; } - public abstract void MoveFileToBottomIfNoOtherPendingMove(string relativeFilename); + public abstract void MoveFileToBottomIfNoOtherPendingMove(FileNode node); public int AddItem(uint itemIdLoc, VSADDITEMOPERATION op, string itemName, uint filesToOpen, string[] files, IntPtr dlgOwner, VSADDRESULT[] result) { @@ -4840,37 +4842,10 @@ public int AddItem(uint itemIdLoc, VSADDITEMOPERATION op, string itemName, uint internal int DoAddItem(uint itemIdLoc, VSADDITEMOPERATION op, string itemName, uint filesToOpen, string[] files, IntPtr dlgOwner, VSADDRESULT[] result, AddItemContext addItemContext = AddItemContext.Unknown) { // Note that when executing UI actions from the F# project system, any pending 'moves' (for add/add above/add below) are already handled at another level (in Project.fs). - // Calls to MoveFileToBottomIfNoOtherPendingMove() in this method are for code paths hit directly by automation APIs. Guid empty = Guid.Empty; // When Adding an item, pass true to let AddItemWithSpecific know to fire the tracker events. - var r = AddItemWithSpecific(itemIdLoc, op, itemName, filesToOpen, files, dlgOwner, 0, ref empty, null, ref empty, result, true, context: addItemContext); - if (op == VSADDITEMOPERATION.VSADDITEMOP_RUNWIZARD) - { - HierarchyNode n = this.NodeFromItemId(itemIdLoc); - string relativeFolder = Path.GetDirectoryName(n.Url); - string relPath = PackageUtilities.MakeRelativeIfRooted(Path.Combine(relativeFolder, Path.GetFileName(itemName)), this.BaseURI); - MoveFileToBottomIfNoOtherPendingMove(relPath); - } - else if (op == VSADDITEMOPERATION.VSADDITEMOP_OPENFILE) - { - foreach (string file in files) - { - HierarchyNode n = this.NodeFromItemId(itemIdLoc); - string relativeFolder = Path.GetDirectoryName(n.Url); - string relPath = PackageUtilities.MakeRelativeIfRooted(Path.Combine(relativeFolder, Path.GetFileName(file)), this.BaseURI); - MoveFileToBottomIfNoOtherPendingMove(relPath); - } - } - else if (op == VSADDITEMOPERATION.VSADDITEMOP_LINKTOFILE) - { - // This does not seem to be reachable from automation APIs, no movement needed. - } - else if (op == VSADDITEMOPERATION.VSADDITEMOP_CLONEFILE) - { - // This seems to only be called as a sub-step of RUNWIZARD, no movement needed. - } - return r; + return AddItemWithSpecific(itemIdLoc, op, itemName, filesToOpen, files, dlgOwner, 0, ref empty, null, ref empty, result, true, context: addItemContext); } /// @@ -4988,11 +4963,22 @@ internal int AddItemWithSpecific(uint itemIdLoc, VSADDITEMOPERATION op, string i case VSADDITEMOPERATION.VSADDITEMOP_OPENFILE: { string fileName = Path.GetFileName(file); - newFileName = + + if (context == AddItemContext.Paste && FindChild(file) != null) + { // if we are doing 'Paste' and source file belongs to current project - generate fresh unique name - context == AddItemContext.Paste && FindChild(file) != null - ? GenerateCopyOfFileName(baseDir, fileName) - : Path.Combine(baseDir, fileName); + newFileName = GenerateCopyOfFileName(baseDir, fileName); + } + else if (!IsContainedWithinProjectDirectory(file)) + { + // if the file isn't contained within the project directory, + // copy it to be a child of the node we're adding to. + newFileName = Path.Combine(baseDir, fileName); + } + else + { + newFileName = file; + } } break; } @@ -5162,6 +5148,14 @@ internal int AddItemWithSpecific(uint itemIdLoc, VSADDITEMOPERATION op, string i } } + for (int i = 0; i < actualFilesAddedIndex; ++i) + { + string absolutePath = actualFiles[i]; + var fileNode = this.FindChild(absolutePath) as FileNode; + Debug.Assert(fileNode != null, $"Unable to find added child node {absolutePath}"); + MoveFileToBottomIfNoOtherPendingMove(fileNode); + } + return VSConstants.S_OK; } @@ -5173,25 +5167,12 @@ public virtual int AddLinkedItem(HierarchyNode node, string[] files, VSADDRESULT // files[index] will be the absolute location to the linked file for (int index = 0; index < files.Length; index++) { - string relativeUri = PackageUtilities.GetPathDistance(this.ProjectMgr.BaseURI.Uri, new Uri(files[index])); - if (string.IsNullOrEmpty(relativeUri)) - { - return VSConstants.E_FAIL; - } - - string fileName = Path.GetFileName(files[index]); - if (string.IsNullOrEmpty(fileName)) - { - return VSConstants.E_FAIL; - } - - LinkedFileNode linkedNode = this.AddNewFileNodeToHierarchy(node, fileName) as LinkedFileNode; + LinkedFileNode linkedNode = this.AddNewFileNodeToHierarchyCore(node, files[index]) as LinkedFileNode; if (linkedNode == null) { return VSConstants.E_FAIL; } - - linkedNode.ItemNode.Rename(relativeUri); + if (node == this) { // parent we are adding to is project root @@ -5205,6 +5186,12 @@ public virtual int AddLinkedItem(HierarchyNode node, string[] files, VSADDRESULT } linkedNode.SetIsLinkedFile(true); linkedNode.OnInvalidateItems(node); + + // fire the node added event now that we've set the item metadata + FireAddNodeEvent(files[index]); + + MoveFileToBottomIfNoOtherPendingMove(linkedNode); + result[0] = VSADDRESULT.ADDRESULT_Success; } return VSConstants.S_OK; @@ -5402,7 +5389,7 @@ public virtual int RemoveItem(uint reserved, uint itemId, out int result) { throw new ArgumentException(SR.GetString(SR.ParameterMustBeAValidItemId, CultureInfo.CurrentUICulture), "itemId"); } - n.Remove(true); + n.Remove(removeFromStorage: true, promptSave: false); result = 1; return VSConstants.S_OK; } @@ -6395,6 +6382,15 @@ private static void CloseAllSubNodes(HierarchyNode node) CloseAllNodes(n); } } + + /// + /// Debug method to assert that the project file and the solution explorer are in sync. + /// + [Conditional("DEBUG")] + public virtual void EnsureMSBuildAndSolutionExplorerAreInSync() + { + } + /// /// Get the project extensions /// diff --git a/vsintegration/src/FSharp.ProjectSystem.Base/Project/ProjectReferenceNode.cs b/vsintegration/src/FSharp.ProjectSystem.Base/Project/ProjectReferenceNode.cs index 2ac7ff2858e0a19c3eedfede8ae90149c63e9ae6..0a951030a9ce2c875d0b9595577f7b3322af8c63 100644 --- a/vsintegration/src/FSharp.ProjectSystem.Base/Project/ProjectReferenceNode.cs +++ b/vsintegration/src/FSharp.ProjectSystem.Base/Project/ProjectReferenceNode.cs @@ -660,14 +660,14 @@ public override bool AddReference() /// /// Overridden method. The method updates the build dependency list before removing the node from the hierarchy. /// - public override void Remove(bool removeFromStorage) + public override void Remove(bool removeFromStorage, bool promptSave = true) { if (this.ProjectMgr == null || !this.canRemoveReference) { return; } this.ProjectMgr.RemoveBuildDependency(this.buildDependency); - base.Remove(removeFromStorage); + base.Remove(removeFromStorage, promptSave); // current reference is removed - delete associated error from list CleanProjectReferenceErrorState(); diff --git a/vsintegration/src/FSharp.ProjectSystem.Base/Project/VSProjectConstants.cs b/vsintegration/src/FSharp.ProjectSystem.Base/Project/VSProjectConstants.cs index 91f7e9c56516b76cfbab9eb0c38b060bd7bf734f..ac97eb678f589466b4c8241e30da79537237a774 100644 --- a/vsintegration/src/FSharp.ProjectSystem.Base/Project/VSProjectConstants.cs +++ b/vsintegration/src/FSharp.ProjectSystem.Base/Project/VSProjectConstants.cs @@ -20,6 +20,8 @@ internal sealed class VSProjectConstants public static readonly CommandID AddNewItemAbove = new CommandID(guidFSharpProjectCmdSet, 0x3005); public static readonly CommandID AddExistingItemAbove = new CommandID(guidFSharpProjectCmdSet, 0x3006); public static readonly CommandID MoveDownCmd = new CommandID(guidFSharpProjectCmdSet, 0x3007); + public static readonly CommandID NewFolderAbove = new CommandID(guidFSharpProjectCmdSet, 0x3008); + public static readonly CommandID NewFolderBelow = new CommandID(guidFSharpProjectCmdSet, 0x3009); public static readonly CommandID FSharpSendThisReferenceToInteractiveCmd = new CommandID(guidFSharpProjectCmdSet, 0x5004); public static readonly CommandID FSharpSendReferencesToInteractiveCmd = new CommandID(guidFSharpProjectCmdSet, 0x5005); diff --git a/vsintegration/src/FSharp.ProjectSystem.FSharp/MSBuildUtilities.fs b/vsintegration/src/FSharp.ProjectSystem.FSharp/MSBuildUtilities.fs index d56c7b27c10ea4e5386b52836d8d9cb8ea434e17..475e68978f9e83e91689c6354d6afeac4df2ccab 100644 --- a/vsintegration/src/FSharp.ProjectSystem.FSharp/MSBuildUtilities.fs +++ b/vsintegration/src/FSharp.ProjectSystem.FSharp/MSBuildUtilities.fs @@ -25,7 +25,11 @@ type internal MSBuildUtilities() = static let GetItemType(item : ProjectItemElement) = item.ItemType - + + /// Normalize path directory separator characters to the OS defacto + static let NormalizePath(path : string) = + path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar) + // Gets the <... Include="path"> path for this item, except if the item is a link, then // gets the path value instead. // In other words, gets the location that will be displayed in the solution explorer. @@ -35,6 +39,7 @@ type internal MSBuildUtilities() = item.EvaluatedInclude else strPath + static let GetUnescapedUnevaluatedInclude(item : ProjectItemElement) = let mutable foundLink = None for m in item.Metadata do @@ -45,17 +50,16 @@ type internal MSBuildUtilities() = match foundLink with | None -> item.Include | Some(link) -> link - ProjectCollection.Unescape(escaped) + ProjectCollection.Unescape(escaped) |> NormalizePath static let MattersForOrdering(bi : ProjectItemElement) = not (bi.ItemType = ProjectFileConstants.ProjectReference || bi.ItemType = ProjectFileConstants.Reference) // if 'path' is as in <... Include="path">, determine the relative path of the folder that contains this static let ComputeFolder(path : string, projectUrl : Url) = - Path.GetDirectoryName(PackageUtilities.MakeRelativeIfRooted(path, projectUrl)) + "\\" + Path.GetDirectoryName(PackageUtilities.MakeRelativeIfRooted(path, projectUrl)) + Path.DirectorySeparatorChar.ToString() - static let FolderComparer = StringComparer.OrdinalIgnoreCase - static let FilenameComparer = StringComparer.OrdinalIgnoreCase + static let FolderComparer = StringComparer.OrdinalIgnoreCase static let Same(x : ProjectItemElement, y : ProjectItemElement) = Object.ReferenceEquals(x,y) @@ -191,13 +195,7 @@ type internal MSBuildUtilities() = match priorGroupWithAtLeastOneItemThatMattersForOrdering with | Some(g) -> EnsureProperFolderLogic msbuildProject g projectNode throwIfCannotRender | None -> msbuildProject.Xml.AddItemGroup() - - static let CheckItemType(item, buildItemName) = - // It checks if this node item has the same BuildActionType as returned by DefaultBuildAction(), which only can see the file name. - // Additionally, return true when a node item has "None" as "default" build action to avoid "compile" or "publish". - let itemType = GetItemType(item) - itemType = "None" || itemType = buildItemName - + static member ThrowIfNotValidAndRearrangeIfNecessary (projectNode : ProjectNode) = EnsureValid projectNode.BuildProject projectNode true |> ignore @@ -205,67 +203,40 @@ type internal MSBuildUtilities() = // TODO wildcards? big.RemoveChild(item) big.InsertBeforeChild(item, itemToMoveAbove) - - /// Move <... Include='relativeFileName'> to above nodeToMoveAbove (from solution-explorer point-of-view) - static member MoveFileAbove(relativeFileName : string, nodeToMoveAbove : HierarchyNode, projectNode : ProjectNode) = - let msbuildProject = projectNode.BuildProject - let buildItemName = projectNode.DefaultBuildAction(relativeFileName) - let big = EnsureValid msbuildProject projectNode true - let mutable itemToMove = None - for bi in EnumerateItems(big) do - if CheckItemType(bi, buildItemName) && 0=FilenameComparer.Compare(GetUnescapedUnevaluatedInclude(bi), relativeFileName) then - itemToMove <- Some(bi) - Debug.Assert(itemToMove.IsSome, "did not find item") - let itemToMoveAbove = nodeToMoveAbove.ItemNode.Item - Debug.Assert(itemToMoveAbove <> null, "nodeToMoveAbove was unexpectedly virtual") // add new/existing item above only works on files, not folders - MSBuildUtilities.MoveFileAboveHelper(itemToMove.Value, itemToMoveAbove.Xml, big, projectNode) - + static member private MoveFileBelowHelper(item : ProjectItemElement, itemToMoveBelow : ProjectItemElement, big : ProjectItemGroupElement, _projectNode : ProjectNode) = // TODO wildcards? big.RemoveChild(item) big.InsertAfterChild(item, itemToMoveBelow) - - static member MoveFileBelowCore(relativeFileName : string, itemToMoveBelow : ProjectItemElement, projectNode : ProjectNode, throwIfCannotRender) = - let msbuildProject = projectNode.BuildProject - let buildItemName = projectNode.DefaultBuildAction(relativeFileName) - let big = EnsureValid msbuildProject projectNode throwIfCannotRender - let mutable itemToMove = None - for bi in EnumerateItems(big) do - if CheckItemType(bi, buildItemName) && 0=FilenameComparer.Compare(GetUnescapedUnevaluatedInclude(bi), relativeFileName) then - itemToMove <- Some(bi) - Debug.Assert(itemToMove.IsSome, "did not find item") - Debug.Assert(itemToMoveBelow <> null, "nodeToMoveBelow was unexpectedly virtual") // add new/existing item below only works on files, not folders - MSBuildUtilities.MoveFileBelowHelper(itemToMove.Value, itemToMoveBelow, big, projectNode) - - /// Move <... Include='relativeFileName'> to below nodeToMoveBelow (from solution-explorer point-of-view) - static member MoveFileBelow(relativeFileName : string, nodeToMoveBelow : HierarchyNode, projectNode : ProjectNode) = - let itemToMoveBelow = nodeToMoveBelow.ItemNode.Item - MSBuildUtilities.MoveFileBelowCore(relativeFileName, itemToMoveBelow.Xml, projectNode, true) - - /// Move <... Include='relativeFileName'> to the bottom of the list of items, except if this item has a subfolder that already exists, move it - /// to the bottom of that subforlder, rather than the very bottom. - static member MoveFileToBottomOfGroup(relativeFileName : string, projectNode : ProjectNode) = - let dir = Path.GetDirectoryName(relativeFileName) + "\\" - let mutable lastItemInDir = null + + /// Move a file node to its correct place in the build project. + /// Its correct place is defined by where it sits in the hierarchy relative + /// to other files. + static member SyncWithHierarchy(fileNode : FileNode) = + let projectNode = fileNode.ProjectMgr let msbuildProject = projectNode.BuildProject - let buildItemName = projectNode.DefaultBuildAction(relativeFileName) let big = EnsureValid msbuildProject projectNode false - let mutable itemToMove = None - for bi in EnumerateItems(big) do - if CheckItemType(bi, buildItemName) && 0=FilenameComparer.Compare(GetUnescapedUnevaluatedInclude(bi), relativeFileName) then - itemToMove <- Some(bi) - else - // under else, as we don't want to try to move under _ourself_, only under _another_ existing item in same dir - if GetUnescapedUnevaluatedInclude(bi).StartsWith(dir, System.StringComparison.OrdinalIgnoreCase) then - lastItemInDir <- bi - Debug.Assert(itemToMove.IsSome, "did not find item") - if lastItemInDir <> null then - MSBuildUtilities.MoveFileBelowCore(relativeFileName, lastItemInDir, projectNode, false) - else - big.RemoveChild(itemToMove.Value) - big.AppendChild(itemToMove.Value) - - + + let itemToMove = fileNode.ItemNode.Item.Xml + big.RemoveChild itemToMove + + let precedingFile = + projectNode.AllDescendants + |> Seq.choose (function :? FileNode as fileNode -> Some fileNode | _ -> None) + |> Seq.takeWhile ((<>) fileNode) + |> Seq.tryLast + + match precedingFile with + | None -> + // if there is no preceding file, it must be because we're + // the first file in the hierarchy - so put us first + big.PrependChild itemToMove + | Some precedingFile -> + big.InsertAfterChild (itemToMove, precedingFile.ItemNode.Item.Xml) + + // The project file should now be in a valid state + MSBuildUtilities.ThrowIfNotValidAndRearrangeIfNecessary projectNode + /// Given a HierarchyNode, compute the last BuildItem if we want to move something after it static member private FindLast(toMoveAfter : HierarchyNode, projectNode : ProjectNode) = match toMoveAfter with @@ -334,8 +305,6 @@ type internal MSBuildUtilities() = index := !index + 1] if !itemToMoveBeforeIndex = -1 then Debug.Assert(false, sprintf "did not find item to move before <%s Include=\"%s\">" (GetItemType itemToMoveBefore) (GetUnescapedUnevaluatedInclude itemToMoveBefore)) - if itemsToMove.IsEmpty then - Debug.Assert(false, sprintf "did not find any item to move (anything in folder %s)" folderToBeMoved) for (item,i) in itemsToMove do Debug.Assert(i <> 0, "item is already at top") Debug.Assert(!itemToMoveBeforeIndex < i, "not moving up") @@ -363,8 +332,6 @@ type internal MSBuildUtilities() = index := !index + 1] if !itemToMoveAfterIndex = -1 then Debug.Assert(false, sprintf "did not find item to move after <%s Include=\"%s\">" (GetItemType itemToMoveAfter) (GetUnescapedUnevaluatedInclude itemToMoveAfter)) - if itemsToMove.IsEmpty then - Debug.Assert(false, sprintf "did not find any item to move (anything in folder %s)" folderToBeMoved) for (item,i) in List.rev itemsToMove do Debug.Assert(i <> !index - 1, "item is already at bottom") Debug.Assert(!itemToMoveAfterIndex > i, "not moving down") diff --git a/vsintegration/src/FSharp.ProjectSystem.FSharp/MenusAndCommands.vsct b/vsintegration/src/FSharp.ProjectSystem.FSharp/MenusAndCommands.vsct index e88c5e412c079a9c9518d19d13b592536524ef9e..94860b7f650eb9689a1ba7aa3685e6030f69d022 100644 --- a/vsintegration/src/FSharp.ProjectSystem.FSharp/MenusAndCommands.vsct +++ b/vsintegration/src/FSharp.ProjectSystem.FSharp/MenusAndCommands.vsct @@ -4,7 +4,8 @@ - + + @@ -179,6 +180,28 @@ Add Existing Item + + @@ -204,6 +227,8 @@ + + diff --git a/vsintegration/src/FSharp.ProjectSystem.FSharp/Project.fs b/vsintegration/src/FSharp.ProjectSystem.FSharp/Project.fs index 657c8c0b10ef97cec919a890cbd86c012516fec7..d88da701a0d3cb476bc15d4e07d8c1ba1828f5b2 100644 --- a/vsintegration/src/FSharp.ProjectSystem.FSharp/Project.fs +++ b/vsintegration/src/FSharp.ProjectSystem.FSharp/Project.fs @@ -684,7 +684,7 @@ namespace rec Microsoft.VisualStudio.FSharp.ProjectSystem vsProject [] - member this.EnsureMSBuildAndSolutionExplorerAreInSync() = + override this.EnsureMSBuildAndSolutionExplorerAreInSync() = let AllSolutionExplorerFilenames() = let rec Compute (node : HierarchyNode, accum) = if obj.ReferenceEquals(node,null) then @@ -735,13 +735,19 @@ namespace rec Microsoft.VisualStudio.FSharp.ProjectSystem else base.GetGuidProperty(propid, &guid) - member fshProjNode.MoveNewlyAddedFileSomehow<'a>(move : (*relativeFileName*)string -> unit, f : unit -> 'a) : 'a = + member fshProjNode.MoveNewlyAddedFileSomehow<'a>(move : FSharpFileNode -> unit, f : unit -> 'a) : 'a = Debug.Assert(addFilesNotification.IsNone, "bad use of addFilesNotification") - addFilesNotification <- Some(fun files -> + addFilesNotification <- Some (fun files -> Debug.Assert(files.Length = 1) let absoluteFileName = files.[0] - let relativeFileName = PackageUtilities.MakeRelativeIfRooted(absoluteFileName, fshProjNode.BaseURI) - move(relativeFileName)) + + match fshProjNode.FindChild absoluteFileName with + | :? FSharpFileNode as fileNode -> + move fileNode + | node -> + let relativeFileName = PackageUtilities.MakeRelativeIfRooted(absoluteFileName, fshProjNode.BaseURI) + Debug.Assert(false, sprintf "Expected to find newly added FSharpFileNode in hierarchy '%s', but found '%O'" relativeFileName node) + ) try let r = f() fshProjNode.ComputeSourcesAndFlags() @@ -750,21 +756,30 @@ namespace rec Microsoft.VisualStudio.FSharp.ProjectSystem addFilesNotification <- None member fshProjNode.MoveNewlyAddedFileAbove<'a>(nodeToMoveAbove : HierarchyNode, f : unit -> 'a) : 'a = - fshProjNode.MoveNewlyAddedFileSomehow((fun relativeFileName -> MSBuildUtilities.MoveFileAbove(relativeFileName, nodeToMoveAbove, fshProjNode) - FSharpFileNode.MoveLastToAbove(nodeToMoveAbove, fshProjNode) |> ignore) - , f) + fshProjNode.MoveNewlyAddedFileSomehow((fun fileNode -> + FSharpFileNode.MoveToBottomOfGroup(fileNode) + FSharpFileNode.MoveTo(Above, nodeToMoveAbove, fileNode) + MSBuildUtilities.SyncWithHierarchy(fileNode) + ), f) member fshProjNode.MoveNewlyAddedFileBelow<'a>(nodeToMoveBelow : HierarchyNode, f : unit -> 'a) : 'a = - fshProjNode.MoveNewlyAddedFileSomehow((fun relativeFileName -> MSBuildUtilities.MoveFileBelow(relativeFileName, nodeToMoveBelow, fshProjNode) - FSharpFileNode.MoveLastToBelow(nodeToMoveBelow, fshProjNode) |> ignore) - , f) + fshProjNode.MoveNewlyAddedFileSomehow((fun fileNode -> + FSharpFileNode.MoveToBottomOfGroup(fileNode) + FSharpFileNode.MoveTo(Below, nodeToMoveBelow, fileNode) + MSBuildUtilities.SyncWithHierarchy(fileNode) + ), f) member fshProjNode.MoveNewlyAddedFileToBottomOfGroup<'a> (f : unit -> 'a) : 'a = - fshProjNode.MoveNewlyAddedFileSomehow((fun relativeFileName -> MSBuildUtilities.MoveFileToBottomOfGroup(relativeFileName, fshProjNode)), f) + fshProjNode.MoveNewlyAddedFileSomehow((fun fileNode -> + FSharpFileNode.MoveToBottomOfGroup(fileNode) + MSBuildUtilities.SyncWithHierarchy(fileNode) + ), f) - override fshProjNode.MoveFileToBottomIfNoOtherPendingMove(relativeFileName) = + override fshProjNode.MoveFileToBottomIfNoOtherPendingMove(fileNode) = match addFilesNotification with - | None -> MSBuildUtilities.MoveFileToBottomOfGroup(relativeFileName, fshProjNode) + | None -> + FSharpFileNode.MoveToBottomOfGroup(fileNode) + MSBuildUtilities.SyncWithHierarchy(fileNode) | Some _ -> () override fshProjNode.ExecCommandOnNode(guidCmdGroup:Guid, cmd:uint32, nCmdexecopt:uint32, pvaIn:IntPtr, pvaOut:IntPtr ) = @@ -794,13 +809,7 @@ namespace rec Microsoft.VisualStudio.FSharp.ProjectSystem base.ExecCommandOnNode(guidCmdGroup, cmd, nCmdexecopt, pvaIn, pvaOut) override fshProjNode.QueryStatusOnNode(guidCmdGroup : Guid, cmd : UInt32, pCmdText : IntPtr, result : byref) = - if guidCmdGroup = VsMenus.guidStandardCommandSet97 then - if (cmd |> int32 |> enum) = Microsoft.VisualStudio.VSConstants.VSStd97CmdID.NewFolder then - result <- result ||| QueryStatusResult.SUPPORTED ||| QueryStatusResult.INVISIBLE - VSConstants.S_OK - else - base.QueryStatusOnNode(guidCmdGroup, cmd, pCmdText, &result) - elif guidCmdGroup = VsMenus.guidStandardCommandSet2K then + if guidCmdGroup = VsMenus.guidStandardCommandSet2K then match (cmd |> int32 |> enum) : VSConstants.VSStd2KCmdID with | _ when cmd = MyVSConstants.ExploreFolderInWindows -> result <- result ||| QueryStatusResult.SUPPORTED ||| QueryStatusResult.ENABLED @@ -1976,32 +1985,128 @@ namespace rec Microsoft.VisualStudio.FSharp.ProjectSystem inherit FolderNode(root, relativePath, projectElement) override x.QueryStatusOnNode(guidCmdGroup:Guid, cmd:uint32, pCmdText:IntPtr, result:byref) = + + let accessor = x.ProjectMgr.Site.GetService(typeof) :?> IVsBuildManagerAccessor + let noBuildInProgress = not(VsBuildManagerAccessorExtensionMethods.IsInProgress(accessor)) + if (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && - (cmd = (uint32)VSProjectConstants.MoveUpCmd.ID) then - result <- result ||| QueryStatusResult.SUPPORTED - if FSharpFileNode.CanMoveUp(x) then - result <- result ||| QueryStatusResult.ENABLED - VSConstants.S_OK + (cmd = (uint32)VSProjectConstants.MoveUpCmd.ID) then + + result <- result ||| QueryStatusResult.SUPPORTED + if FSharpFileNode.CanMoveUp(x) then + result <- result ||| QueryStatusResult.ENABLED + VSConstants.S_OK + elif (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && - (cmd = (uint32)VSProjectConstants.MoveDownCmd.ID) then - result <- result ||| QueryStatusResult.SUPPORTED - if FSharpFileNode.CanMoveDown(x) then - result <- result ||| QueryStatusResult.ENABLED - VSConstants.S_OK + (cmd = (uint32)VSProjectConstants.MoveDownCmd.ID) then + + result <- result ||| QueryStatusResult.SUPPORTED + if FSharpFileNode.CanMoveDown(x) then + result <- result ||| QueryStatusResult.ENABLED + VSConstants.S_OK + + elif (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && + (cmd = (uint32)VSProjectConstants.AddExistingItemAbove.ID) then + + result <- result ||| QueryStatusResult.SUPPORTED + if noBuildInProgress && root.GetSelectedNodes().Count < 2 then + result <- result ||| QueryStatusResult.ENABLED + VSConstants.S_OK + + elif (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && + (cmd = (uint32)VSProjectConstants.AddNewItemAbove.ID) then + + result <- result ||| QueryStatusResult.SUPPORTED + if noBuildInProgress && root.GetSelectedNodes().Count < 2 then + result <- result ||| QueryStatusResult.ENABLED + VSConstants.S_OK + + elif (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && + (cmd = (uint32)VSProjectConstants.AddExistingItemBelow.ID) then + + result <- result ||| QueryStatusResult.SUPPORTED + if noBuildInProgress && root.GetSelectedNodes().Count < 2 then + result <- result ||| QueryStatusResult.ENABLED + VSConstants.S_OK + + elif (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && + (cmd = (uint32)VSProjectConstants.AddNewItemBelow.ID) then + + result <- result ||| QueryStatusResult.SUPPORTED + if noBuildInProgress && root.GetSelectedNodes().Count < 2 then + result <- result ||| QueryStatusResult.ENABLED + VSConstants.S_OK + + elif (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && + (cmd = (uint32)VSProjectConstants.NewFolderAbove.ID) then + + result <- result ||| QueryStatusResult.SUPPORTED + if noBuildInProgress && root.GetSelectedNodes().Count < 2 then + result <- result ||| QueryStatusResult.ENABLED + VSConstants.S_OK + + elif (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && + (cmd = (uint32)VSProjectConstants.NewFolderBelow.ID) then + + result <- result ||| QueryStatusResult.SUPPORTED + if noBuildInProgress && root.GetSelectedNodes().Count < 2 then + result <- result ||| QueryStatusResult.ENABLED + VSConstants.S_OK + else - base.QueryStatusOnNode(guidCmdGroup, cmd, pCmdText, &result) + base.QueryStatusOnNode(guidCmdGroup, cmd, pCmdText, &result) override x.ExecCommandOnNode(guidCmdGroup:Guid, cmd:uint32, nCmdexecopt:uint32, pvaIn:IntPtr, pvaOut:IntPtr ) = if (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && - (cmd = (uint32)VSProjectConstants.MoveUpCmd.ID) then - FSharpFileNode.MoveUp(x, root) - VSConstants.S_OK + (cmd = (uint32)VSProjectConstants.MoveUpCmd.ID) then + FSharpFileNode.MoveUp(x, root) + VSConstants.S_OK + elif (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && - (cmd = (uint32)VSProjectConstants.MoveDownCmd.ID) then - FSharpFileNode.MoveDown(x, root) - VSConstants.S_OK + (cmd = (uint32)VSProjectConstants.MoveDownCmd.ID) then + FSharpFileNode.MoveDown(x, root) + VSConstants.S_OK + + elif (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && + (cmd = (uint32)VSProjectConstants.AddNewItemAbove.ID) then + let result = root.MoveNewlyAddedFileAbove (x, fun () -> + x.Parent.AddItemToHierarchy(HierarchyAddType.AddNewItem)) + root.EnsureMSBuildAndSolutionExplorerAreInSync() + result + + elif (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && + (cmd = (uint32)VSProjectConstants.AddExistingItemAbove.ID) then + let result = root.MoveNewlyAddedFileAbove (x, fun () -> + x.Parent.AddItemToHierarchy(HierarchyAddType.AddExistingItem)) + root.EnsureMSBuildAndSolutionExplorerAreInSync() + result + + elif (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && + (cmd = (uint32)VSProjectConstants.AddNewItemBelow.ID) then + let result = root.MoveNewlyAddedFileBelow (x, fun () -> + x.Parent.AddItemToHierarchy(HierarchyAddType.AddNewItem)) + root.EnsureMSBuildAndSolutionExplorerAreInSync() + result + + elif (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && + (cmd = (uint32)VSProjectConstants.AddExistingItemBelow.ID) then + let result = root.MoveNewlyAddedFileBelow (x, fun () -> + x.Parent.AddItemToHierarchy(HierarchyAddType.AddExistingItem)) + root.EnsureMSBuildAndSolutionExplorerAreInSync() + result + + elif (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && + (cmd = (uint32)VSProjectConstants.NewFolderAbove.ID) then + + x.Parent.AddNewFolder(fun newNode -> FSharpFileNode.MoveTo(Above, x, newNode)) + + elif (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && + (cmd = (uint32)VSProjectConstants.NewFolderBelow.ID) then + + x.Parent.AddNewFolder(fun newNode -> FSharpFileNode.MoveTo(Below, x, newNode)) + else - base.ExecCommandOnNode(guidCmdGroup, cmd, nCmdexecopt, pvaIn, pvaOut) + base.ExecCommandOnNode(guidCmdGroup, cmd, nCmdexecopt, pvaIn, pvaOut) type internal FSharpBuildAction = | None = 0 @@ -2065,7 +2170,10 @@ namespace rec Microsoft.VisualStudio.FSharp.ProjectSystem if (not(fileNameEditable) && (propertyDescriptor.Name = "FileName")) then Microsoft.VisualStudio.Editors.PropertyPages.FilteredObjectWrapper.ReadOnlyPropertyDescriptorWrapper(propertyDescriptor) :> PropertyDescriptor else base.CreateDesignPropertyDescriptor(propertyDescriptor) - + + type internal InsertionLocation = + | Above + | Below /// Represents most (non-reference) nodes in the solution hierarchy of an F# project (e.g. foo.fs, bar.fsi, app.config) type internal FSharpFileNode(root:FSharpProjectNode, e:ProjectElement, hierarchyId) = @@ -2097,6 +2205,18 @@ namespace rec Microsoft.VisualStudio.FSharp.ProjectSystem Some(new SelectionElementValueChangedListener(sp)) + /// Unlink a node from its siblings. + static let unlinkFromSiblings (node : HierarchyNode) = + match node.PreviousSibling with + | null -> + node.Parent.FirstChild <- node.NextSibling + | previous -> + previous.NextSibling <- node.NextSibling + if node.Parent.LastChild = node then + node.Parent.LastChild <- node.PreviousSibling + node.NextSibling <- null + node.OnItemDeleted() + do selectionChangedListener.Value.Init() override x.IsNonMemberItem with get() = false @@ -2240,7 +2360,107 @@ namespace rec Microsoft.VisualStudio.FSharp.ProjectSystem lastNode.NextSibling <- tmp root.OnItemAdded(lastNode.Parent, lastNode) lastNode :?> FSharpFileNode + + /// Move a node to above/below the 'target node' in the hierarchy. + /// If it is not valid for the node to be directly below the 'target node', + /// a warning dialog will be shown. + static member MoveTo(location : InsertionLocation, targetNode : HierarchyNode, nodeToBeMoved : HierarchyNode) : unit = + let root = nodeToBeMoved.ProjectMgr + Debug.Assert(targetNode.ProjectMgr = nodeToBeMoved.ProjectMgr) + + // if targetNode and nodeToBeMoved are not siblings, try to find + // the (grand)parent of nodeToBeMoved that is a sibling + let rec tryFindTargetNodeSibling = + function + | (null : HierarchyNode) -> + None + | node when node.Parent = targetNode.Parent -> + Some node + | node -> + tryFindTargetNodeSibling node.Parent + + let isFileNode : HierarchyNode -> bool = + function + | :? FSharpFileNode -> true + | _ -> false + + match tryFindTargetNodeSibling nodeToBeMoved with + | Some siblingNode when siblingNode <> nodeToBeMoved -> + let fileChildren = siblingNode.AllDescendants |> Seq.filter isFileNode |> List.ofSeq + if fileChildren = [nodeToBeMoved] then + Ok siblingNode + else + Error <| String.Format(FSharpSR.GetString(FSharpSR.FileCannotBePlacedMultipleFiles), siblingNode.VirtualNodeName) + | Some siblingNode -> + Ok siblingNode + | None -> + Error <| FSharpSR.GetString(FSharpSR.FileCannotBePlacedDifferentSubtree) + |> function + | Ok node -> + unlinkFromSiblings node + + match location with + | Above -> + match targetNode.PreviousSibling with + | null -> targetNode.Parent.FirstChild <- node + | prev -> prev.NextSibling <- node + + node.NextSibling <- targetNode + | Below -> + match targetNode.NextSibling with + | null -> targetNode.Parent.LastChild <- node + | next -> node.NextSibling <- next + + targetNode.NextSibling <- node + + root.OnItemAdded(node.Parent, node) + | Error message -> + // If it is not called from an automation method show a dialog box + if Utilities.IsInAutomationFunction(root.Site) then + raise <| InvalidOperationException message + else + let title = null + let icon = OLEMSGICON.OLEMSGICON_WARNING + let buttons = OLEMSGBUTTON.OLEMSGBUTTON_OK + let defaultButton = OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST + + let relPath = PackageUtilities.MakeRelativeIfRooted(nodeToBeMoved.Url, root.BaseURI) + let relTargetPath = PackageUtilities.MakeRelativeIfRooted(targetNode.Url, root.BaseURI) + + let bodyString = + match location with + | Above -> FSharpSR.FileCannotBePlacedBodyAbove + | Below -> FSharpSR.FileCannotBePlacedBodyBelow + |> FSharpSR.GetStringWithCR + + let entireMessage = String.Format(bodyString, relPath, relTargetPath, message) + VsShellUtilities.ShowMessageBox(root.Site, title, entireMessage, icon, buttons, defaultButton) |> ignore + + /// Move the node to the bottom of its subfolder within the Solution Explorer. + /// If its directory hierarchy does not exist, create it. + static member MoveToBottomOfGroup(node : HierarchyNode) : unit = + match node with + | :? FSharpFileNode as fileNode -> + let root = fileNode.ProjectMgr + unlinkFromSiblings fileNode + + let rec tryFindAdoptiveParent (currentPath : string list, remainingPath : string list, currentParent : HierarchyNode) = + match remainingPath with + | [] -> + currentParent + | folderName::restPath -> + let path = currentPath @ [folderName] + let pathStr = String.concat "\\" path + let folderNode = root.VerifySubFolderExists(pathStr + "\\", currentParent) + tryFindAdoptiveParent (path, restPath, folderNode) + + let pathParts = Path.GetDirectoryName(fileNode.RelativeFilePath).Split([| Path.DirectorySeparatorChar |], StringSplitOptions.RemoveEmptyEntries) + let parent = tryFindAdoptiveParent ([], List.ofArray pathParts, root) + parent.AddChild(fileNode) + | _ -> + Debug.Assert(false, sprintf "Unable to find FSharpFileNode '%s'" node.Url) + override x.ExecCommandOnNode(guidCmdGroup:Guid, cmd:uint32, nCmdexecopt:uint32, pvaIn:IntPtr, pvaOut:IntPtr ) = Debug.Assert(x.ProjectMgr <> null, "The FSharpFileNode has no project manager") @@ -2270,30 +2490,41 @@ namespace rec Microsoft.VisualStudio.FSharp.ProjectSystem elif (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && (cmd = (uint32)VSProjectConstants.AddNewItemAbove.ID) then let result = root.MoveNewlyAddedFileAbove (x, fun () -> - root.AddItemToHierarchy(HierarchyAddType.AddNewItem)) + x.AddItemToHierarchy(HierarchyAddType.AddNewItem)) root.EnsureMSBuildAndSolutionExplorerAreInSync() result elif (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && (cmd = (uint32)VSProjectConstants.AddExistingItemAbove.ID) then let result = root.MoveNewlyAddedFileAbove (x, fun () -> - root.AddItemToHierarchy(HierarchyAddType.AddExistingItem)) + x.AddItemToHierarchy(HierarchyAddType.AddExistingItem)) root.EnsureMSBuildAndSolutionExplorerAreInSync() result elif (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && (cmd = (uint32)VSProjectConstants.AddNewItemBelow.ID) then let result = root.MoveNewlyAddedFileBelow (x, fun () -> - root.AddItemToHierarchy(HierarchyAddType.AddNewItem)) + x.AddItemToHierarchy(HierarchyAddType.AddNewItem)) root.EnsureMSBuildAndSolutionExplorerAreInSync() result elif (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && (cmd = (uint32)VSProjectConstants.AddExistingItemBelow.ID) then let result = root.MoveNewlyAddedFileBelow (x, fun () -> - root.AddItemToHierarchy(HierarchyAddType.AddExistingItem)) + x.AddItemToHierarchy(HierarchyAddType.AddExistingItem)) root.EnsureMSBuildAndSolutionExplorerAreInSync() result + + + elif (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && + (cmd = (uint32)VSProjectConstants.NewFolderAbove.ID) then + + x.Parent.AddNewFolder(fun newNode -> FSharpFileNode.MoveTo(Above, x, newNode)) + + elif (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && + (cmd = (uint32)VSProjectConstants.NewFolderBelow.ID) then + + x.Parent.AddNewFolder(fun newNode -> FSharpFileNode.MoveTo(Below, x, newNode)) else base.ExecCommandOnNode(guidCmdGroup, cmd, nCmdexecopt, pvaIn, pvaOut) @@ -2383,6 +2614,24 @@ namespace rec Microsoft.VisualStudio.FSharp.ProjectSystem result <- result ||| QueryStatusResult.ENABLED VSConstants.S_OK + | _ when + (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && + (cmd = (uint32)VSProjectConstants.NewFolderAbove.ID) -> + + result <- result ||| QueryStatusResult.SUPPORTED + if noBuildInProgress && root.GetSelectedNodes().Count < 2 then + result <- result ||| QueryStatusResult.ENABLED + VSConstants.S_OK + + | _ when + (guidCmdGroup = VSProjectConstants.guidFSharpProjectCmdSet) && + (cmd = (uint32)VSProjectConstants.NewFolderBelow.ID) -> + + result <- result ||| QueryStatusResult.SUPPORTED + if noBuildInProgress && root.GetSelectedNodes().Count < 2 then + result <- result ||| QueryStatusResult.ENABLED + VSConstants.S_OK + | _ -> base.QueryStatusOnNode(guidCmdGroup, cmd, pCmdText, &result) static member CanMoveDown(node : HierarchyNode) = @@ -2512,15 +2761,7 @@ namespace rec Microsoft.VisualStudio.FSharp.ProjectSystem root.SetProjectFileDirty(true) // Recompute & notify of changes root.ComputeSourcesAndFlags() - - member x.GetRelativePath() = - let mutable relativePath = Path.GetFileName(x.ItemNode.GetMetadata(ProjectFileConstants.Include)) - let mutable parent = x.Parent - while (parent <> null && not (parent :? ProjectNode)) do - relativePath <- Path.Combine(parent.Caption, relativePath) - parent <- parent.Parent - relativePath - + member x.ServiceCreator : OleServiceProvider.ServiceCreatorCallback = new OleServiceProvider.ServiceCreatorCallback(x.CreateServices) diff --git a/vsintegration/src/FSharp.ProjectSystem.FSharp/ProjectPrelude.fs b/vsintegration/src/FSharp.ProjectSystem.FSharp/ProjectPrelude.fs index 8a831237ae66b65c827dd5373639ccc584a78636..ca30ed2803ccc3673e0ac455efed3801c6e91e77 100644 --- a/vsintegration/src/FSharp.ProjectSystem.FSharp/ProjectPrelude.fs +++ b/vsintegration/src/FSharp.ProjectSystem.FSharp/ProjectPrelude.fs @@ -221,7 +221,14 @@ namespace Microsoft.VisualStudio.FSharp.ProjectSystem let ComputingSourcesAndFlags = "ComputingSourcesAndFlags" [] let UpdatingSolutionConfiguration = "UpdatingSolutionConfiguration" - + [] + let FileCannotBePlacedBodyAbove = "FileCannotBePlacedBodyAbove" + [] + let FileCannotBePlacedBodyBelow = "FileCannotBePlacedBodyBelow" + [] + let FileCannotBePlacedDifferentSubtree = "FileCannotBePlacedDifferentSubtree" + [] + let FileCannotBePlacedMultipleFiles = "FileCannotBePlacedMultipleFiles" type private TypeInThisAssembly = class end let thisAssembly = typeof.Assembly diff --git a/vsintegration/src/FSharp.ProjectSystem.FSharp/VSPackage.resx b/vsintegration/src/FSharp.ProjectSystem.FSharp/VSPackage.resx index 35e43fc838578d6c4db6b90b2a607312b787752a..fa0160256fd3a401fdee712ee5eb8489ae5a401f 100644 --- a/vsintegration/src/FSharp.ProjectSystem.FSharp/VSPackage.resx +++ b/vsintegration/src/FSharp.ProjectSystem.FSharp/VSPackage.resx @@ -530,4 +530,16 @@ Updating solution configuration... + + The file '{0}' cannot be placed above '{1}' in the Solution Explorer.\n\n{2}. + + + The file '{0}' cannot be placed below '{1}' in the Solution Explorer.\n\n{2}. + + + The file is in a different subtree + + + The '{0}' folder cannot be moved as it already exists in the Solution Explorer in a different location + \ No newline at end of file diff --git a/vsintegration/tests/unittests/Tests.ProjectSystem.Miscellaneous.fs b/vsintegration/tests/unittests/Tests.ProjectSystem.Miscellaneous.fs index ddfbc35cc436bfce20cf69a4b0b05e892b032286..1531e48c0bf6d9a6420f99a3f09d6c46ba5ff78d 100644 --- a/vsintegration/tests/unittests/Tests.ProjectSystem.Miscellaneous.fs +++ b/vsintegration/tests/unittests/Tests.ProjectSystem.Miscellaneous.fs @@ -88,14 +88,14 @@ type Miscellaneous() = ) [] - member public this.``Miscellaneous.FSharpFileNode.GetRelativePath`` () = + member public this.``Miscellaneous.FSharpFileNode.RelativeFilePath`` () = this.MakeProjectAndDo(["orig1.fs"], [], "", (fun project -> let absFilePath = Path.Combine(project.ProjectFolder, "orig1.fs") let files = new List() project.FindNodesOfType(files) Assert.AreEqual(1, files.Count) let file = files.[0] - let path = file.GetRelativePath() + let path = file.RelativeFilePath Assert.AreEqual("orig1.fs", path) )) diff --git a/vsintegration/tests/unittests/Tests.ProjectSystem.Project.fs b/vsintegration/tests/unittests/Tests.ProjectSystem.Project.fs index e4f7a0e17295597e69894e58b0d4d75184e303ae..75f596191795294ea24c68245a2952e5a94bc4e8 100644 --- a/vsintegration/tests/unittests/Tests.ProjectSystem.Project.fs +++ b/vsintegration/tests/unittests/Tests.ProjectSystem.Project.fs @@ -47,7 +47,7 @@ type Project() = l.[0] [] - member public this.NoNewFolderOnProjectMenu() = + member public this.NewFolderOnProjectMenu() = printfn "starting..." let package = new FSharpProjectPackage() let project = new FSharpProjectNode(package) @@ -59,8 +59,8 @@ type Project() = let x = project.QueryStatusOnNode(guidCmdGroup, uint32(cmdEnum), pCmdText, &result) printfn "and..." AssertEqual x VSConstants.S_OK - if (result &&& QueryStatusResult.INVISIBLE) = enum 0 then - Assert.Fail("Unexpected: New Folder was not invisible") + if (result &&& QueryStatusResult.ENABLED) <> QueryStatusResult.ENABLED then + Assert.Fail("Unexpected: New Folder was not enabled") () [] diff --git a/vsintegration/tests/unittests/Tests.ProjectSystem.ProjectItems.fs b/vsintegration/tests/unittests/Tests.ProjectSystem.ProjectItems.fs index dcc96dd7b146c149db77b209e1989009d0b92b7b..a0626dad2888094b9f8b7e8178a4e20605dba22b 100644 --- a/vsintegration/tests/unittests/Tests.ProjectSystem.ProjectItems.fs +++ b/vsintegration/tests/unittests/Tests.ProjectSystem.ProjectItems.fs @@ -75,26 +75,6 @@ type ProjectItems() = File.Delete(absFilePath) )) - [] - member public this.``AddNewItem.ItemAppearsAtBottomOfFsprojFileEvenIfUnknownItemWithSameName``() = - this.MakeProjectAndDo([], [], @" - - - - - ", (fun project -> - let absFilePath = Path.Combine(project.ProjectFolder, "a.fs") - try - File.AppendAllText(absFilePath, "#light") - // Note: this is not the same code path as the UI, but it is close - project.MoveNewlyAddedFileToBottomOfGroup (fun () -> - project.AddNewFileNodeToHierarchy(project,absFilePath) |> ignore) - let msbuildInfo = TheTests.MsBuildCompileItems(project.BuildProject) - AssertEqual ["orig.fs"; "a.fs"] msbuildInfo - finally - File.Delete(absFilePath) - )) - [] member public this.``AddNewItemBelow.ItemAppearsInRightSpot``() = this.MakeProjectAndDo(["orig1.fs"; "orig2.fs"], [], "", (fun project ->