提交 dd18b3e0 编写于 作者: S Saul Rennison 提交者: Kevin Ransom (msft)

Improved project folder support (#2692)

* Significantly improved project folder support

Fixes #2178

* Better support for non-Windows style directory separators in .fsproj

* Enable support for "Add folder" on project node

* Fix copy/pasting files within subfolders

Fixes #2048

* "Add existing" on the project node now places the file in the correct subfolder

* "Add existing..." now creates any necessary intermediate directories

* Improved resilience of Add Above/Add Below with folders

* Localised new error messages

* Add "New Folder" to Add Above/Add Below

* Updated FileCannotBePlacedMultipleFiles localisation to be more user-friendly

* Fixed being unable to "Add existing" on files outside of the project hierarchy

* Rename AllChildren to AllDescendants

* Fix cut/paste folders resulting in an infinite loop

https://mpfproj10.codeplex.com/workitem/11618

* Fix test compile error

* Make InsertionLocation internal

* Add support for linked files

"Add Existing" on files outside of the project hierarchy will now copy them to the target node

* If the folder exists on disk use that instead

* Do not delete linked files when deleting folders

* Remove bogus test - it's not possible to add a file to the project that has the same name as an existing file
上级 4c31706e
...@@ -394,7 +394,7 @@ public override bool CanDeleteItem(__VSDELETEITEMOPERATION deleteOperation) ...@@ -394,7 +394,7 @@ public override bool CanDeleteItem(__VSDELETEITEMOPERATION deleteOperation)
return base.CanDeleteItem(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 // 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 // we disable IVsTrackProjectDocuments2 events to avoid confusing messages from SCC
...@@ -403,7 +403,7 @@ public override void Remove(bool removeFromStorage) ...@@ -403,7 +403,7 @@ public override void Remove(bool removeFromStorage)
{ {
ProjectMgr.EventTriggeringFlag = oldFlag | ProjectNode.EventTriggering.DoNotTriggerTrackerEvents; ProjectMgr.EventTriggeringFlag = oldFlag | ProjectNode.EventTriggering.DoNotTriggerTrackerEvents;
base.Remove(removeFromStorage); base.Remove(removeFromStorage, promptSave);
// invoke ComputeSourcesAndFlags to refresh compiler flags // invoke ComputeSourcesAndFlags to refresh compiler flags
// it was the only useful thing performed by one of IVsTrackProjectDocuments2 listeners // it was the only useful thing performed by one of IVsTrackProjectDocuments2 listeners
......
...@@ -350,7 +350,7 @@ public virtual void Remove() ...@@ -350,7 +350,7 @@ public virtual void Remove()
extensibility.EnterAutomationFunction(); extensibility.EnterAutomationFunction();
try try
{ {
this.node.Remove(false); this.node.Remove(removeFromStorage: false);
} }
finally finally
{ {
...@@ -381,7 +381,7 @@ public virtual void Delete() ...@@ -381,7 +381,7 @@ public virtual void Delete()
try try
{ {
this.node.Remove(true); this.node.Remove(removeFromStorage: true, promptSave: false);
} }
finally finally
{ {
......
...@@ -158,7 +158,7 @@ public virtual string PublicKeyToken ...@@ -158,7 +158,7 @@ public virtual string PublicKeyToken
public virtual void Remove() public virtual void Remove()
{ {
UIThread.DoOnUIThread(delegate(){ UIThread.DoOnUIThread(delegate(){
BaseReferenceNode.Remove(false); BaseReferenceNode.Remove(removeFromStorage: false);
}); });
} }
......
...@@ -210,6 +210,14 @@ internal FileNode(ProjectNode root, ProjectElement element, uint? hierarchyId = ...@@ -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() public override NodeProperties CreatePropertiesObject()
{ {
return new FileNodeProperties(this); return new FileNodeProperties(this);
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
using VsCommands2K = Microsoft.VisualStudio.VSConstants.VSStd2KCmdID; using VsCommands2K = Microsoft.VisualStudio.VSConstants.VSStd2KCmdID;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Microsoft.VisualStudio.FSharp.ProjectSystem namespace Microsoft.VisualStudio.FSharp.ProjectSystem
{ {
...@@ -93,21 +94,15 @@ public override int SetEditLabel(string label) ...@@ -93,21 +94,15 @@ public override int SetEditLabel(string label)
string newPath = Path.Combine(new DirectoryInfo(this.Url).Parent.FullName, 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) for (HierarchyNode n = Parent.FirstChild; n != null; n = n.NextSibling)
{ {
if (n != this && String.Compare(n.Caption, label, StringComparison.OrdinalIgnoreCase) == 0) 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 try
{ {
RenameFolder(label); RenameFolder(label);
...@@ -380,7 +375,8 @@ public virtual void RenameDirectory(string newPath) ...@@ -380,7 +375,8 @@ public virtual void RenameDirectory(string newPath)
{ {
if (Directory.Exists(newPath)) if (Directory.Exists(newPath))
{ {
ShowFileOrFolderAlreadExistsErrorMessage(newPath); ShowErrorMessage(SR.FileOrFolderAlreadyExists, newPath);
return;
} }
Directory.Move(this.Url, newPath); Directory.Move(this.Url, newPath);
...@@ -389,12 +385,34 @@ public virtual void RenameDirectory(string newPath) ...@@ -389,12 +385,34 @@ public virtual void RenameDirectory(string newPath)
private void RenameFolder(string newName) 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 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) 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.VirtualNodeName = newPath;
this.ItemNode.Rename(VirtualNodeName); this.ItemNode.Rename(VirtualNodeName);
...@@ -422,13 +440,15 @@ private void RenameFolder(string newName) ...@@ -422,13 +440,15 @@ private void RenameFolder(string newName)
/// <summary> /// <summary>
/// Show error message if not in automation mode, otherwise throw exception /// Show error message if not in automation mode, otherwise throw exception
/// </summary> /// </summary>
/// <param name="newPath">path of file or folder already existing on disk</param> /// <param name="parameter">Parameter for resource string format</param>
/// <returns>S_OK</returns> /// <returns>S_OK</returns>
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. // Most likely the cause of:
//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. // A file or folder with the name '{0}' already exists on disk at this location. Please choose another name.
string errorMessage = (String.Format(CultureInfo.CurrentCulture, SR.GetString(SR.FileOrFolderAlreadyExists, CultureInfo.CurrentUICulture), newPath)); // -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)) if (!Utilities.IsInAutomationFunction(this.ProjectMgr.Site))
{ {
string title = null; string title = null;
......
...@@ -524,8 +524,9 @@ public virtual void AddChild(HierarchyNode node) ...@@ -524,8 +524,9 @@ public virtual void AddChild(HierarchyNode node)
Object nodeWithSameID = this.projectMgr.ItemIdMap[node.hierarchyId]; Object nodeWithSameID = this.projectMgr.ItemIdMap[node.hierarchyId];
if (!Object.ReferenceEquals(node, nodeWithSameID as HierarchyNode)) 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); this.projectMgr.ItemIdMap.SetAt(node.hierarchyId, this);
} }
else else
...@@ -928,7 +929,7 @@ public virtual string GetMkDocument() ...@@ -928,7 +929,7 @@ public virtual string GetMkDocument()
/// Removes items from the hierarchy. Project overwrites this /// Removes items from the hierarchy. Project overwrites this
/// </summary> /// </summary>
/// <param name="removeFromStorage"></param> /// <param name="removeFromStorage"></param>
public virtual void Remove(bool removeFromStorage) public virtual void Remove(bool removeFromStorage, bool promptSave = true)
{ {
string documentToRemove = this.GetMkDocument(); string documentToRemove = this.GetMkDocument();
...@@ -944,7 +945,7 @@ public virtual void Remove(bool removeFromStorage) ...@@ -944,7 +945,7 @@ public virtual void Remove(bool removeFromStorage)
DocumentManager manager = this.GetDocumentManager(); DocumentManager manager = this.GetDocumentManager();
if (manager != null) 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. // User cancelled operation in message box.
return; return;
...@@ -963,7 +964,7 @@ public virtual void Remove(bool removeFromStorage) ...@@ -963,7 +964,7 @@ public virtual void Remove(bool removeFromStorage)
// Remove child if any before removing from the hierarchy // Remove child if any before removing from the hierarchy
for (HierarchyNode child = this.FirstChild; child != null; child = child.NextSibling) for (HierarchyNode child = this.FirstChild; child != null; child = child.NextSibling)
{ {
child.Remove(removeFromStorage); child.Remove(removeFromStorage: false, promptSave: promptSave);
} }
HierarchyNode thisParentNode = this.parentNode; HierarchyNode thisParentNode = this.parentNode;
...@@ -1114,7 +1115,7 @@ public virtual HierarchyNode GetDragTargetHandlerNode() ...@@ -1114,7 +1115,7 @@ public virtual HierarchyNode GetDragTargetHandlerNode()
/// Add a new Folder to the project hierarchy. /// Add a new Folder to the project hierarchy.
/// </summary> /// </summary>
/// <returns>S_OK if succeeded, otherwise an error</returns> /// <returns>S_OK if succeeded, otherwise an error</returns>
public virtual int AddNewFolder() public virtual int AddNewFolder(Action<HierarchyNode> moveNode=null)
{ {
// Check out the project file. // Check out the project file.
if (!this.ProjectMgr.QueryEditProjectFile(false)) if (!this.ProjectMgr.QueryEditProjectFile(false))
...@@ -1129,20 +1130,25 @@ public virtual int AddNewFolder() ...@@ -1129,20 +1130,25 @@ public virtual int AddNewFolder()
ErrorHandler.ThrowOnFailure(this.projectMgr.GenerateUniqueItemName(this.hierarchyId, String.Empty, String.Empty, out newFolderName)); ErrorHandler.ThrowOnFailure(this.projectMgr.GenerateUniqueItemName(this.hierarchyId, String.Empty, String.Empty, out newFolderName));
// create the project part of it, the project file // 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 we are in automation mode then skip the ui part which is about renaming the folder
if (!Utilities.IsInAutomationFunction(this.projectMgr.Site)) if (!Utilities.IsInAutomationFunction(this.projectMgr.Site))
{ {
IVsUIHierarchyWindow uiWindow = UIHierarchyUtilities.GetUIHierarchyWindow(this.projectMgr.Site, SolutionExplorer); IVsUIHierarchyWindow uiWindow = UIHierarchyUtilities.GetUIHierarchyWindow(this.projectMgr.Site, SolutionExplorer);
// we need to get into label edit mode now... // we need to get into label edit mode now...
// so first select the new guy... // 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 // them post the rename command to the shell. Folder verification and creation will
// happen in the setlabel code... // happen in the setlabel code...
IVsUIShell shell = this.projectMgr.Site.GetService(typeof(SVsUIShell)) as IVsUIShell; IVsUIShell shell = this.projectMgr.Site.GetService(typeof(SVsUIShell)) as IVsUIShell;
...@@ -1214,7 +1220,7 @@ public virtual void DoDefaultAction() ...@@ -1214,7 +1220,7 @@ public virtual void DoDefaultAction()
public virtual int ExcludeFromProject() public virtual int ExcludeFromProject()
{ {
Debug.Assert(this.ProjectMgr != null, "The project item " + this.ToString() + " has not been initialised correctly. It has a null ProjectMgr"); 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; return VSConstants.S_OK;
} }
...@@ -1403,14 +1409,19 @@ public virtual int ExecCommandOnNode(Guid cmdGroup, uint cmd, uint nCmdexecopt, ...@@ -1403,14 +1409,19 @@ public virtual int ExecCommandOnNode(Guid cmdGroup, uint cmd, uint nCmdexecopt,
} }
else if (cmdGroup == VsMenus.guidStandardCommandSet97) else if (cmdGroup == VsMenus.guidStandardCommandSet97)
{ {
int result = -1;
HierarchyNode nodeToAddTo = this.GetDragTargetHandlerNode(); HierarchyNode nodeToAddTo = this.GetDragTargetHandlerNode();
switch ((VsCommands)cmd) switch ((VsCommands)cmd)
{ {
case VsCommands.AddNewItem: case VsCommands.AddNewItem:
return nodeToAddTo.AddItemToHierarchy(HierarchyAddType.AddNewItem); result = nodeToAddTo.AddItemToHierarchy(HierarchyAddType.AddNewItem);
this.projectMgr.EnsureMSBuildAndSolutionExplorerAreInSync();
return result;
case VsCommands.AddExistingItem: case VsCommands.AddExistingItem:
return nodeToAddTo.AddItemToHierarchy(HierarchyAddType.AddExistingItem); result = nodeToAddTo.AddItemToHierarchy(HierarchyAddType.AddExistingItem);
this.projectMgr.EnsureMSBuildAndSolutionExplorerAreInSync();
return result;
case VsCommands.NewFolder: case VsCommands.NewFolder:
return nodeToAddTo.AddNewFolder(); return nodeToAddTo.AddNewFolder();
...@@ -2876,7 +2887,8 @@ public virtual int DeleteItem(uint delItemOp, uint itemId) ...@@ -2876,7 +2887,8 @@ public virtual int DeleteItem(uint delItemOp, uint itemId)
HierarchyNode node = this.projectMgr.NodeFromItemId(itemId); HierarchyNode node = this.projectMgr.NodeFromItemId(itemId);
if (node != null) 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; return VSConstants.S_OK;
} }
...@@ -3294,5 +3306,38 @@ public int GetResourceItem(uint itemidDocument, string pszCulture, uint grfPRF, ...@@ -3294,5 +3306,38 @@ public int GetResourceItem(uint itemidDocument, string pszCulture, uint grfPRF,
} }
public virtual __VSPROVISIONALVIEWINGSTATUS ProvisionalViewingStatus => __VSPROVISIONALVIEWINGSTATUS.PVS_Disabled; public virtual __VSPROVISIONALVIEWINGSTATUS ProvisionalViewingStatus => __VSPROVISIONALVIEWINGSTATUS.PVS_Disabled;
/// <summary>
/// All nodes that are direct children of this node.
/// </summary>
public virtual IEnumerable<HierarchyNode> AllChildren
{
get
{
for (var child = this.FirstChild; child != null; child = child.NextSibling)
{
yield return child;
}
}
}
/// <summary>
/// All nodes that are my children, plus their children, ad infinitum.
/// </summary>
public virtual IEnumerable<HierarchyNode> AllDescendants
{
get
{
foreach (var child in this.AllChildren)
{
yield return child;
foreach (var descendant in child.AllDescendants)
{
yield return descendant;
}
}
}
}
} }
} }
...@@ -158,6 +158,19 @@ public bool IsImported ...@@ -158,6 +158,19 @@ public bool IsImported
public override int MenuCommandId public override int MenuCommandId
{ {
get { return VsMenus.IDM_VS_CTXT_ITEMNODE; } 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;
}
}
} }
} }
...@@ -115,6 +115,7 @@ public sealed class SR ...@@ -115,6 +115,7 @@ public sealed class SR
public const string FileName = "FileName"; public const string FileName = "FileName";
public const string FileNameDescription = "FileNameDescription"; public const string FileNameDescription = "FileNameDescription";
public const string FileOrFolderAlreadyExists = "FileOrFolderAlreadyExists"; public const string FileOrFolderAlreadyExists = "FileOrFolderAlreadyExists";
public const string FolderCannotBeRenamed = "FolderCannotBeRenamed";
public const string FileOrFolderCannotBeFound = "FileOrFolderCannotBeFound"; public const string FileOrFolderCannotBeFound = "FileOrFolderCannotBeFound";
public const string FileProperties = "FileProperties"; public const string FileProperties = "FileProperties";
public const string FolderName = "FolderName"; public const string FolderName = "FolderName";
......
...@@ -407,6 +407,9 @@ ...@@ -407,6 +407,9 @@
<data name="FileOrFolderAlreadyExists" xml:space="preserve"> <data name="FileOrFolderAlreadyExists" xml:space="preserve">
<value>A file or folder with the name '{0}' already exists on disk at this location. Please choose another name.</value> <value>A file or folder with the name '{0}' already exists on disk at this location. Please choose another name.</value>
</data> </data>
<data name="FolderCannotBeRenamed" xml:space="preserve">
<value>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.</value>
</data>
<data name="BuildCaption" xml:space="preserve"> <data name="BuildCaption" xml:space="preserve">
<value>Build</value> <value>Build</value>
</data> </data>
......
...@@ -555,7 +555,7 @@ public virtual void WalkSourceProjectAndAdd(IVsHierarchy sourceHierarchy, uint i ...@@ -555,7 +555,7 @@ public virtual void WalkSourceProjectAndAdd(IVsHierarchy sourceHierarchy, uint i
while (currentItemID != VSConstants.VSITEMID_NIL) while (currentItemID != VSConstants.VSITEMID_NIL)
{ {
variant = null; 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; currentItemID = (uint)(int)variant;
WalkSourceProjectAndAdd(sourceHierarchy, currentItemID, targetNode, true); WalkSourceProjectAndAdd(sourceHierarchy, currentItemID, targetNode, true);
} }
...@@ -974,7 +974,7 @@ public void CleanupSelectionDataObject(bool dropped, bool cut, bool moved, bool ...@@ -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) else if (w != null)
{ {
......
...@@ -1521,7 +1521,7 @@ public override int SetGuidProperty(int propid, ref Guid guid) ...@@ -1521,7 +1521,7 @@ public override int SetGuidProperty(int propid, ref Guid guid)
/// Removes items from the hierarchy. /// Removes items from the hierarchy.
/// </summary> /// </summary>
/// <devdoc>Project overwrites this.</devdoc> /// <devdoc>Project overwrites this.</devdoc>
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 // the project will not be deleted from disk, just removed
if (removeFromStorage) if (removeFromStorage)
...@@ -1529,6 +1529,8 @@ public override void Remove(bool removeFromStorage) ...@@ -1529,6 +1529,8 @@ public override void Remove(bool removeFromStorage)
return; return;
} }
Debug.Assert(promptSave, "Non-save prompting removal is not supported");
// Remove the entire project from the solution // Remove the entire project from the solution
IVsSolution solution = this.Site.GetService(typeof(SVsSolution)) as IVsSolution; IVsSolution solution = this.Site.GetService(typeof(SVsSolution)) as IVsSolution;
uint iOption = 1; // SLNSAVEOPT_PromptSave uint iOption = 1; // SLNSAVEOPT_PromptSave
...@@ -3642,7 +3644,7 @@ internal virtual ProjectElement AddFileToMsBuild(string file) ...@@ -3642,7 +3644,7 @@ internal virtual ProjectElement AddFileToMsBuild(string file)
{ {
ProjectElement newItem; 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."); Debug.Assert(!Path.IsPathRooted(itemPath), "Cannot add item with full path.");
string defaultBuildAction = this.DefaultBuildAction(itemPath); string defaultBuildAction = this.DefaultBuildAction(itemPath);
...@@ -4830,7 +4832,7 @@ public virtual int GetMkDocument(uint itemId, out string mkDoc) ...@@ -4830,7 +4832,7 @@ public virtual int GetMkDocument(uint itemId, out string mkDoc)
return VSConstants.S_OK; 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) 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 ...@@ -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) 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). // 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; Guid empty = Guid.Empty;
// When Adding an item, pass true to let AddItemWithSpecific know to fire the tracker events. // 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); return 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;
} }
/// <summary> /// <summary>
...@@ -4988,11 +4963,22 @@ internal int AddItemWithSpecific(uint itemIdLoc, VSADDITEMOPERATION op, string i ...@@ -4988,11 +4963,22 @@ internal int AddItemWithSpecific(uint itemIdLoc, VSADDITEMOPERATION op, string i
case VSADDITEMOPERATION.VSADDITEMOP_OPENFILE: case VSADDITEMOPERATION.VSADDITEMOP_OPENFILE:
{ {
string fileName = Path.GetFileName(file); 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 // if we are doing 'Paste' and source file belongs to current project - generate fresh unique name
context == AddItemContext.Paste && FindChild(file) != null newFileName = GenerateCopyOfFileName(baseDir, fileName);
? GenerateCopyOfFileName(baseDir, fileName) }
: Path.Combine(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; break;
} }
...@@ -5162,6 +5148,14 @@ internal int AddItemWithSpecific(uint itemIdLoc, VSADDITEMOPERATION op, string i ...@@ -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; return VSConstants.S_OK;
} }
...@@ -5173,25 +5167,12 @@ public virtual int AddLinkedItem(HierarchyNode node, string[] files, VSADDRESULT ...@@ -5173,25 +5167,12 @@ public virtual int AddLinkedItem(HierarchyNode node, string[] files, VSADDRESULT
// files[index] will be the absolute location to the linked file // files[index] will be the absolute location to the linked file
for (int index = 0; index < files.Length; index++) for (int index = 0; index < files.Length; index++)
{ {
string relativeUri = PackageUtilities.GetPathDistance(this.ProjectMgr.BaseURI.Uri, new Uri(files[index])); LinkedFileNode linkedNode = this.AddNewFileNodeToHierarchyCore(node, files[index]) as LinkedFileNode;
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;
if (linkedNode == null) if (linkedNode == null)
{ {
return VSConstants.E_FAIL; return VSConstants.E_FAIL;
} }
linkedNode.ItemNode.Rename(relativeUri);
if (node == this) if (node == this)
{ {
// parent we are adding to is project root // parent we are adding to is project root
...@@ -5205,6 +5186,12 @@ public virtual int AddLinkedItem(HierarchyNode node, string[] files, VSADDRESULT ...@@ -5205,6 +5186,12 @@ public virtual int AddLinkedItem(HierarchyNode node, string[] files, VSADDRESULT
} }
linkedNode.SetIsLinkedFile(true); linkedNode.SetIsLinkedFile(true);
linkedNode.OnInvalidateItems(node); 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; result[0] = VSADDRESULT.ADDRESULT_Success;
} }
return VSConstants.S_OK; return VSConstants.S_OK;
...@@ -5402,7 +5389,7 @@ public virtual int RemoveItem(uint reserved, uint itemId, out int result) ...@@ -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"); throw new ArgumentException(SR.GetString(SR.ParameterMustBeAValidItemId, CultureInfo.CurrentUICulture), "itemId");
} }
n.Remove(true); n.Remove(removeFromStorage: true, promptSave: false);
result = 1; result = 1;
return VSConstants.S_OK; return VSConstants.S_OK;
} }
...@@ -6395,6 +6382,15 @@ private static void CloseAllSubNodes(HierarchyNode node) ...@@ -6395,6 +6382,15 @@ private static void CloseAllSubNodes(HierarchyNode node)
CloseAllNodes(n); CloseAllNodes(n);
} }
} }
/// <summary>
/// Debug method to assert that the project file and the solution explorer are in sync.
/// </summary>
[Conditional("DEBUG")]
public virtual void EnsureMSBuildAndSolutionExplorerAreInSync()
{
}
/// <summary> /// <summary>
/// Get the project extensions /// Get the project extensions
/// </summary> /// </summary>
......
...@@ -660,14 +660,14 @@ public override bool AddReference() ...@@ -660,14 +660,14 @@ public override bool AddReference()
/// <summary> /// <summary>
/// Overridden method. The method updates the build dependency list before removing the node from the hierarchy. /// Overridden method. The method updates the build dependency list before removing the node from the hierarchy.
/// </summary> /// </summary>
public override void Remove(bool removeFromStorage) public override void Remove(bool removeFromStorage, bool promptSave = true)
{ {
if (this.ProjectMgr == null || !this.canRemoveReference) if (this.ProjectMgr == null || !this.canRemoveReference)
{ {
return; return;
} }
this.ProjectMgr.RemoveBuildDependency(this.buildDependency); this.ProjectMgr.RemoveBuildDependency(this.buildDependency);
base.Remove(removeFromStorage); base.Remove(removeFromStorage, promptSave);
// current reference is removed - delete associated error from list // current reference is removed - delete associated error from list
CleanProjectReferenceErrorState(); CleanProjectReferenceErrorState();
......
...@@ -20,6 +20,8 @@ internal sealed class VSProjectConstants ...@@ -20,6 +20,8 @@ internal sealed class VSProjectConstants
public static readonly CommandID AddNewItemAbove = new CommandID(guidFSharpProjectCmdSet, 0x3005); public static readonly CommandID AddNewItemAbove = new CommandID(guidFSharpProjectCmdSet, 0x3005);
public static readonly CommandID AddExistingItemAbove = new CommandID(guidFSharpProjectCmdSet, 0x3006); public static readonly CommandID AddExistingItemAbove = new CommandID(guidFSharpProjectCmdSet, 0x3006);
public static readonly CommandID MoveDownCmd = new CommandID(guidFSharpProjectCmdSet, 0x3007); 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 FSharpSendThisReferenceToInteractiveCmd = new CommandID(guidFSharpProjectCmdSet, 0x5004);
public static readonly CommandID FSharpSendReferencesToInteractiveCmd = new CommandID(guidFSharpProjectCmdSet, 0x5005); public static readonly CommandID FSharpSendReferencesToInteractiveCmd = new CommandID(guidFSharpProjectCmdSet, 0x5005);
......
...@@ -25,7 +25,11 @@ type internal MSBuildUtilities() = ...@@ -25,7 +25,11 @@ type internal MSBuildUtilities() =
static let GetItemType(item : ProjectItemElement) = static let GetItemType(item : ProjectItemElement) =
item.ItemType 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 <... Include="path"> path for this item, except if the item is a link, then
// gets the <Link>path</Link> value instead. // gets the <Link>path</Link> value instead.
// In other words, gets the location that will be displayed in the solution explorer. // In other words, gets the location that will be displayed in the solution explorer.
...@@ -35,6 +39,7 @@ type internal MSBuildUtilities() = ...@@ -35,6 +39,7 @@ type internal MSBuildUtilities() =
item.EvaluatedInclude item.EvaluatedInclude
else else
strPath strPath
static let GetUnescapedUnevaluatedInclude(item : ProjectItemElement) = static let GetUnescapedUnevaluatedInclude(item : ProjectItemElement) =
let mutable foundLink = None let mutable foundLink = None
for m in item.Metadata do for m in item.Metadata do
...@@ -45,17 +50,16 @@ type internal MSBuildUtilities() = ...@@ -45,17 +50,16 @@ type internal MSBuildUtilities() =
match foundLink with match foundLink with
| None -> item.Include | None -> item.Include
| Some(link) -> link | Some(link) -> link
ProjectCollection.Unescape(escaped) ProjectCollection.Unescape(escaped) |> NormalizePath
static let MattersForOrdering(bi : ProjectItemElement) = static let MattersForOrdering(bi : ProjectItemElement) =
not (bi.ItemType = ProjectFileConstants.ProjectReference || bi.ItemType = ProjectFileConstants.Reference) 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 // if 'path' is as in <... Include="path">, determine the relative path of the folder that contains this
static let ComputeFolder(path : string, projectUrl : Url) = 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 FolderComparer = StringComparer.OrdinalIgnoreCase
static let FilenameComparer = StringComparer.OrdinalIgnoreCase
static let Same(x : ProjectItemElement, y : ProjectItemElement) = static let Same(x : ProjectItemElement, y : ProjectItemElement) =
Object.ReferenceEquals(x,y) Object.ReferenceEquals(x,y)
...@@ -191,13 +195,7 @@ type internal MSBuildUtilities() = ...@@ -191,13 +195,7 @@ type internal MSBuildUtilities() =
match priorGroupWithAtLeastOneItemThatMattersForOrdering with match priorGroupWithAtLeastOneItemThatMattersForOrdering with
| Some(g) -> EnsureProperFolderLogic msbuildProject g projectNode throwIfCannotRender | Some(g) -> EnsureProperFolderLogic msbuildProject g projectNode throwIfCannotRender
| None -> msbuildProject.Xml.AddItemGroup() | 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) = static member ThrowIfNotValidAndRearrangeIfNecessary (projectNode : ProjectNode) =
EnsureValid projectNode.BuildProject projectNode true |> ignore EnsureValid projectNode.BuildProject projectNode true |> ignore
...@@ -205,67 +203,40 @@ type internal MSBuildUtilities() = ...@@ -205,67 +203,40 @@ type internal MSBuildUtilities() =
// TODO wildcards? // TODO wildcards?
big.RemoveChild(item) big.RemoveChild(item)
big.InsertBeforeChild(item, itemToMoveAbove) 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) = static member private MoveFileBelowHelper(item : ProjectItemElement, itemToMoveBelow : ProjectItemElement, big : ProjectItemGroupElement, _projectNode : ProjectNode) =
// TODO wildcards? // TODO wildcards?
big.RemoveChild(item) big.RemoveChild(item)
big.InsertAfterChild(item, itemToMoveBelow) big.InsertAfterChild(item, itemToMoveBelow)
static member MoveFileBelowCore(relativeFileName : string, itemToMoveBelow : ProjectItemElement, projectNode : ProjectNode, throwIfCannotRender) = /// Move a file node to its correct place in the build project.
let msbuildProject = projectNode.BuildProject /// Its correct place is defined by where it sits in the hierarchy relative
let buildItemName = projectNode.DefaultBuildAction(relativeFileName) /// to other files.
let big = EnsureValid msbuildProject projectNode throwIfCannotRender static member SyncWithHierarchy(fileNode : FileNode) =
let mutable itemToMove = None let projectNode = fileNode.ProjectMgr
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
let msbuildProject = projectNode.BuildProject let msbuildProject = projectNode.BuildProject
let buildItemName = projectNode.DefaultBuildAction(relativeFileName)
let big = EnsureValid msbuildProject projectNode false let big = EnsureValid msbuildProject projectNode false
let mutable itemToMove = None
for bi in EnumerateItems(big) do let itemToMove = fileNode.ItemNode.Item.Xml
if CheckItemType(bi, buildItemName) && 0=FilenameComparer.Compare(GetUnescapedUnevaluatedInclude(bi), relativeFileName) then big.RemoveChild itemToMove
itemToMove <- Some(bi)
else let precedingFile =
// under else, as we don't want to try to move under _ourself_, only under _another_ existing item in same dir projectNode.AllDescendants
if GetUnescapedUnevaluatedInclude(bi).StartsWith(dir, System.StringComparison.OrdinalIgnoreCase) then |> Seq.choose (function :? FileNode as fileNode -> Some fileNode | _ -> None)
lastItemInDir <- bi |> Seq.takeWhile ((<>) fileNode)
Debug.Assert(itemToMove.IsSome, "did not find item") |> Seq.tryLast
if lastItemInDir <> null then
MSBuildUtilities.MoveFileBelowCore(relativeFileName, lastItemInDir, projectNode, false) match precedingFile with
else | None ->
big.RemoveChild(itemToMove.Value) // if there is no preceding file, it must be because we're
big.AppendChild(itemToMove.Value) // 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 /// Given a HierarchyNode, compute the last BuildItem if we want to move something after it
static member private FindLast(toMoveAfter : HierarchyNode, projectNode : ProjectNode) = static member private FindLast(toMoveAfter : HierarchyNode, projectNode : ProjectNode) =
match toMoveAfter with match toMoveAfter with
...@@ -334,8 +305,6 @@ type internal MSBuildUtilities() = ...@@ -334,8 +305,6 @@ type internal MSBuildUtilities() =
index := !index + 1] index := !index + 1]
if !itemToMoveBeforeIndex = -1 then if !itemToMoveBeforeIndex = -1 then
Debug.Assert(false, sprintf "did not find item to move before <%s Include=\"%s\">" (GetItemType itemToMoveBefore) (GetUnescapedUnevaluatedInclude itemToMoveBefore)) 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 for (item,i) in itemsToMove do
Debug.Assert(i <> 0, "item is already at top") Debug.Assert(i <> 0, "item is already at top")
Debug.Assert(!itemToMoveBeforeIndex < i, "not moving up") Debug.Assert(!itemToMoveBeforeIndex < i, "not moving up")
...@@ -363,8 +332,6 @@ type internal MSBuildUtilities() = ...@@ -363,8 +332,6 @@ type internal MSBuildUtilities() =
index := !index + 1] index := !index + 1]
if !itemToMoveAfterIndex = -1 then if !itemToMoveAfterIndex = -1 then
Debug.Assert(false, sprintf "did not find item to move after <%s Include=\"%s\">" (GetItemType itemToMoveAfter) (GetUnescapedUnevaluatedInclude itemToMoveAfter)) 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 for (item,i) in List.rev itemsToMove do
Debug.Assert(i <> !index - 1, "item is already at bottom") Debug.Assert(i <> !index - 1, "item is already at bottom")
Debug.Assert(!itemToMoveAfterIndex > i, "not moving down") Debug.Assert(!itemToMoveAfterIndex > i, "not moving down")
......
...@@ -4,7 +4,8 @@ ...@@ -4,7 +4,8 @@
<Extern href="stdidcmd.h"/> <Extern href="stdidcmd.h"/>
<Extern href="vsshlids.h"/> <Extern href="vsshlids.h"/>
<Include href="..\FSharp.VS.FSI\fsiCommands.vsct"/> <!-- FSI-LINKAGE-POINT --> <Include href="..\FSharp.VS.FSI\fsiCommands.vsct"/> <!-- FSI-LINKAGE-POINT -->
<Include href="KnownImageIds.vsct"/>
<!-- Stake a claime to existing commands so they aren't marked as unused and removed. --> <!-- Stake a claime to existing commands so they aren't marked as unused and removed. -->
<UsedCommands> <UsedCommands>
<UsedCommand guid="guidVSStd2K" id="ECMD_ADDREFERENCE" /> <UsedCommand guid="guidVSStd2K" id="ECMD_ADDREFERENCE" />
...@@ -179,6 +180,28 @@ ...@@ -179,6 +180,28 @@
<CommandName>Add Existing Item</CommandName> <CommandName>Add Existing Item</CommandName>
</Strings> </Strings>
</Button> </Button>
<Button guid="FSharpProjectCmdSet" id="NewFolderAbove" priority="0x200" type="Button">
<Parent guid ="FSharpProjectCmdSet" id ="FSharpGroupAddAbove"/>
<Icon guid="ImageCatalogGuid" id="NewFolder"/>
<CommandFlag>DynamicVisibility</CommandFlag>
<CommandFlag>DefaultInvisible</CommandFlag>
<CommandFlag>TextChanges</CommandFlag>
<CommandFlag>IconIsMoniker</CommandFlag>
<Strings>
<ButtonText>New Fol&amp;der</ButtonText>
</Strings>
</Button>
<Button guid="FSharpProjectCmdSet" id="NewFolderBelow" priority="0x200" type="Button">
<Parent guid ="FSharpProjectCmdSet" id ="FSharpGroupAddBelow"/>
<Icon guid="ImageCatalogGuid" id="NewFolder"/>
<CommandFlag>DynamicVisibility</CommandFlag>
<CommandFlag>DefaultInvisible</CommandFlag>
<CommandFlag>TextChanges</CommandFlag>
<CommandFlag>IconIsMoniker</CommandFlag>
<Strings>
<ButtonText>New Fol&amp;der</ButtonText>
</Strings>
</Button>
</Buttons> </Buttons>
<Bitmaps> <Bitmaps>
<Bitmap guid="FSharpMoveUpBmp" href="Resources\MoveUp.bmp" usedList="moveUp"/> <Bitmap guid="FSharpMoveUpBmp" href="Resources\MoveUp.bmp" usedList="moveUp"/>
...@@ -204,6 +227,8 @@ ...@@ -204,6 +227,8 @@
<IDSymbol name ="AddNewItemAbove" value ="0x3005"/> <IDSymbol name ="AddNewItemAbove" value ="0x3005"/>
<IDSymbol name ="AddExistingItemAbove" value ="0x3006"/> <IDSymbol name ="AddExistingItemAbove" value ="0x3006"/>
<IDSymbol name ="MoveDownCmd" value ="0x3007"/> <IDSymbol name ="MoveDownCmd" value ="0x3007"/>
<IDSymbol name ="NewFolderAbove" value ="0x3008"/>
<IDSymbol name ="NewFolderBelow" value ="0x3009"/>
<IDSymbol name = "FSharpSendThisReferenceToInteractiveGroup" value = "0x5001"/> <IDSymbol name = "FSharpSendThisReferenceToInteractiveGroup" value = "0x5001"/>
<IDSymbol name = "FSharpSendReferencesToInteractiveGroup" value = "0x5002"/> <IDSymbol name = "FSharpSendReferencesToInteractiveGroup" value = "0x5002"/>
......
...@@ -221,7 +221,14 @@ namespace Microsoft.VisualStudio.FSharp.ProjectSystem ...@@ -221,7 +221,14 @@ namespace Microsoft.VisualStudio.FSharp.ProjectSystem
let ComputingSourcesAndFlags = "ComputingSourcesAndFlags" let ComputingSourcesAndFlags = "ComputingSourcesAndFlags"
[<Literal>] [<Literal>]
let UpdatingSolutionConfiguration = "UpdatingSolutionConfiguration" let UpdatingSolutionConfiguration = "UpdatingSolutionConfiguration"
[<Literal>]
let FileCannotBePlacedBodyAbove = "FileCannotBePlacedBodyAbove"
[<Literal>]
let FileCannotBePlacedBodyBelow = "FileCannotBePlacedBodyBelow"
[<Literal>]
let FileCannotBePlacedDifferentSubtree = "FileCannotBePlacedDifferentSubtree"
[<Literal>]
let FileCannotBePlacedMultipleFiles = "FileCannotBePlacedMultipleFiles"
type private TypeInThisAssembly = class end type private TypeInThisAssembly = class end
let thisAssembly = typeof<TypeInThisAssembly>.Assembly let thisAssembly = typeof<TypeInThisAssembly>.Assembly
......
...@@ -530,4 +530,16 @@ ...@@ -530,4 +530,16 @@
<data name="UpdatingSolutionConfiguration" xml:space="preserve"> <data name="UpdatingSolutionConfiguration" xml:space="preserve">
<value>Updating solution configuration...</value> <value>Updating solution configuration...</value>
</data> </data>
<data name="FileCannotBePlacedBodyAbove" xml:space="preserve">
<value>The file '{0}' cannot be placed above '{1}' in the Solution Explorer.\n\n{2}.</value>
</data>
<data name="FileCannotBePlacedBodyBelow" xml:space="preserve">
<value>The file '{0}' cannot be placed below '{1}' in the Solution Explorer.\n\n{2}.</value>
</data>
<data name="FileCannotBePlacedDifferentSubtree" xml:space="preserve">
<value>The file is in a different subtree</value>
</data>
<data name="FileCannotBePlacedMultipleFiles" xml:space="preserve">
<value>The '{0}' folder cannot be moved as it already exists in the Solution Explorer in a different location</value>
</data>
</root> </root>
\ No newline at end of file
...@@ -88,14 +88,14 @@ type Miscellaneous() = ...@@ -88,14 +88,14 @@ type Miscellaneous() =
) )
[<Test>] [<Test>]
member public this.``Miscellaneous.FSharpFileNode.GetRelativePath`` () = member public this.``Miscellaneous.FSharpFileNode.RelativeFilePath`` () =
this.MakeProjectAndDo(["orig1.fs"], [], "", (fun project -> this.MakeProjectAndDo(["orig1.fs"], [], "", (fun project ->
let absFilePath = Path.Combine(project.ProjectFolder, "orig1.fs") let absFilePath = Path.Combine(project.ProjectFolder, "orig1.fs")
let files = new List<FSharpFileNode>() let files = new List<FSharpFileNode>()
project.FindNodesOfType(files) project.FindNodesOfType(files)
Assert.AreEqual(1, files.Count) Assert.AreEqual(1, files.Count)
let file = files.[0] let file = files.[0]
let path = file.GetRelativePath() let path = file.RelativeFilePath
Assert.AreEqual("orig1.fs", path) Assert.AreEqual("orig1.fs", path)
)) ))
......
...@@ -47,7 +47,7 @@ type Project() = ...@@ -47,7 +47,7 @@ type Project() =
l.[0] l.[0]
[<Test>] [<Test>]
member public this.NoNewFolderOnProjectMenu() = member public this.NewFolderOnProjectMenu() =
printfn "starting..." printfn "starting..."
let package = new FSharpProjectPackage() let package = new FSharpProjectPackage()
let project = new FSharpProjectNode(package) let project = new FSharpProjectNode(package)
...@@ -59,8 +59,8 @@ type Project() = ...@@ -59,8 +59,8 @@ type Project() =
let x = project.QueryStatusOnNode(guidCmdGroup, uint32(cmdEnum), pCmdText, &result) let x = project.QueryStatusOnNode(guidCmdGroup, uint32(cmdEnum), pCmdText, &result)
printfn "and..." printfn "and..."
AssertEqual x VSConstants.S_OK AssertEqual x VSConstants.S_OK
if (result &&& QueryStatusResult.INVISIBLE) = enum 0 then if (result &&& QueryStatusResult.ENABLED) <> QueryStatusResult.ENABLED then
Assert.Fail("Unexpected: New Folder was not invisible") Assert.Fail("Unexpected: New Folder was not enabled")
() ()
[<Test>] [<Test>]
......
...@@ -75,26 +75,6 @@ type ProjectItems() = ...@@ -75,26 +75,6 @@ type ProjectItems() =
File.Delete(absFilePath) File.Delete(absFilePath)
)) ))
[<Test>]
member public this.``AddNewItem.ItemAppearsAtBottomOfFsprojFileEvenIfUnknownItemWithSameName``() =
this.MakeProjectAndDo([], [], @"
<ItemGroup>
<Unknown Include=""a.fs"" />
<Compile Include=""orig.fs"" />
</ItemGroup>
", (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)
))
[<Test>] [<Test>]
member public this.``AddNewItemBelow.ItemAppearsInRightSpot``() = member public this.``AddNewItemBelow.ItemAppearsInRightSpot``() =
this.MakeProjectAndDo(["orig1.fs"; "orig2.fs"], [], "", (fun project -> this.MakeProjectAndDo(["orig1.fs"; "orig2.fs"], [], "", (fun project ->
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册