Skip to content

Commit

Permalink
Improved project folder support (#2692)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
saul authored and KevinRansom committed Apr 20, 2017
1 parent 4c31706 commit dd18b3e
Show file tree
Hide file tree
Showing 21 changed files with 562 additions and 242 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ public virtual void Remove()
extensibility.EnterAutomationFunction();
try
{
this.node.Remove(false);
this.node.Remove(removeFromStorage: false);
}
finally
{
Expand Down Expand Up @@ -381,7 +381,7 @@ public virtual void Delete()

try
{
this.node.Remove(true);
this.node.Remove(removeFromStorage: true, promptSave: false);
}
finally
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public virtual string PublicKeyToken
public virtual void Remove()
{
UIThread.DoOnUIThread(delegate(){
BaseReferenceNode.Remove(false);
BaseReferenceNode.Remove(removeFromStorage: false);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
52 changes: 36 additions & 16 deletions vsintegration/src/FSharp.ProjectSystem.Base/Project/FolderNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -422,13 +440,15 @@ private void RenameFolder(string newName)
/// <summary>
/// Show error message if not in automation mode, otherwise throw exception
/// </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>
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -928,7 +929,7 @@ public virtual string GetMkDocument()
/// Removes items from the hierarchy. Project overwrites this
/// </summary>
/// <param name="removeFromStorage"></param>
public virtual void Remove(bool removeFromStorage)
public virtual void Remove(bool removeFromStorage, bool promptSave = true)
{
string documentToRemove = this.GetMkDocument();

Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -1114,7 +1115,7 @@ public virtual HierarchyNode GetDragTargetHandlerNode()
/// Add a new Folder to the project hierarchy.
/// </summary>
/// <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.
if (!this.ProjectMgr.QueryEditProjectFile(false))
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -3294,5 +3306,38 @@ public int GetResourceItem(uint itemidDocument, string pszCulture, uint grfPRF,
}

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;
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,9 @@
<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>
</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">
<value>Build</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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)
{
Expand Down
Loading

0 comments on commit dd18b3e

Please sign in to comment.