diff --git a/src/Community.VisualStudio.Toolkit.Shared/Events/SelectionEvents.cs b/src/Community.VisualStudio.Toolkit.Shared/Events/SelectionEvents.cs index 130a396..faa1880 100644 --- a/src/Community.VisualStudio.Toolkit.Shared/Events/SelectionEvents.cs +++ b/src/Community.VisualStudio.Toolkit.Shared/Events/SelectionEvents.cs @@ -37,8 +37,8 @@ int IVsSelectionEvents.OnSelectionChanged(IVsHierarchy pHierOld, uint itemidOld, { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - IVsHierarchyItem? from = await pHierOld.ToHierarcyItemAsync(itemidOld); - IVsHierarchyItem? to = await pHierNew.ToHierarcyItemAsync(itemidNew); + ItemNode? from = await ItemNode.CreateAsync(pHierOld, itemidOld); + ItemNode? to = await ItemNode.CreateAsync(pHierNew, itemidNew); SelectionChanged?.Invoke(this, new SelectionChangedEventArgs(from, to)); }).FireAndForget(); @@ -66,7 +66,7 @@ public class SelectionChangedEventArgs : EventArgs /// /// Creates a new instance of the EventArgs. /// - public SelectionChangedEventArgs(IVsHierarchyItem? from, IVsHierarchyItem? to) + public SelectionChangedEventArgs(ItemNode? from, ItemNode? to) { From = from; To = to; @@ -75,11 +75,11 @@ public SelectionChangedEventArgs(IVsHierarchyItem? from, IVsHierarchyItem? to) /// /// What the selection was before the change. /// - public IVsHierarchyItem? From { get; } + public ItemNode? From { get; } /// /// What the selection is currently after the change. /// - public IVsHierarchyItem? To { get; } + public ItemNode? To { get; } } } diff --git a/src/Community.VisualStudio.Toolkit.Shared/ExtensionMethods/IVsHierarchyExtensions.cs b/src/Community.VisualStudio.Toolkit.Shared/ExtensionMethods/IVsHierarchyExtensions.cs index a36e794..8c09827 100644 --- a/src/Community.VisualStudio.Toolkit.Shared/ExtensionMethods/IVsHierarchyExtensions.cs +++ b/src/Community.VisualStudio.Toolkit.Shared/ExtensionMethods/IVsHierarchyExtensions.cs @@ -20,17 +20,7 @@ public static class IVsHierarchyExtensions /// public static bool TryGetItemProperty(this IVsHierarchy hierarchy, uint itemId, int propertyId, out T? value) { - ThreadHelper.ThrowIfNotOnUIThread(); - - if (ErrorHandler.Failed(hierarchy.GetProperty(itemId, propertyId, out var property)) || !(property is T)) - { - value = default; - return false; - } - - value = (T)property; - - return true; + return HierarchyUtilities.TryGetHierarchyProperty(hierarchy, itemId, propertyId, out value); } /// diff --git a/src/Community.VisualStudio.Toolkit.Shared/Helpers/ProjectTypes.cs b/src/Community.VisualStudio.Toolkit.Shared/Helpers/ProjectTypes.cs index 6610271..0798de2 100644 --- a/src/Community.VisualStudio.Toolkit.Shared/Helpers/ProjectTypes.cs +++ b/src/Community.VisualStudio.Toolkit.Shared/Helpers/ProjectTypes.cs @@ -1,4 +1,4 @@ -namespace EnvDTE +namespace Community.VisualStudio.Toolkit { /// /// A list of known project types guids. diff --git a/src/Community.VisualStudio.Toolkit.Shared/ItemNode.cs b/src/Community.VisualStudio.Toolkit.Shared/ItemNode.cs new file mode 100644 index 0000000..24e926b --- /dev/null +++ b/src/Community.VisualStudio.Toolkit.Shared/ItemNode.cs @@ -0,0 +1,217 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Internal.VisualStudio.PlatformUI; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; + +namespace Community.VisualStudio.Toolkit +{ + /// + /// A node reprensenting a file, folder, project or other item in Solution Explorer. + /// + [DebuggerDisplay("{Name} ({Type})")] + public class ItemNode + { + private ItemNode? _parent; + private IEnumerable? _children; + private IVsHierarchy _hierarchy; + private uint _itemId; + + private ItemNode(IVsHierarchyItem item) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + _hierarchy = item.HierarchyIdentity.IsNestedItem ? item.HierarchyIdentity.NestedHierarchy : item.HierarchyIdentity.Hierarchy; + _itemId = item.HierarchyIdentity.IsNestedItem ? item.HierarchyIdentity.NestedItemID : item.HierarchyIdentity.ItemID; + + _hierarchy.GetCanonicalName(_itemId, out var fileName); + + FileName = fileName; + Item = item; + Name = item.Text; + Type = GetNodeType(item.HierarchyIdentity); + IsExpandable = HierarchyUtilities.IsExpandable(item.HierarchyIdentity); + IsFaulted = HierarchyUtilities.IsFaultedProject(item.HierarchyIdentity); + IsHidden = HierarchyUtilities.IsHiddenItem(_hierarchy, _itemId); + IsRoot = item.HierarchyIdentity.IsRoot; + IsNested = item.HierarchyIdentity.IsNestedItem; + } + + /// + /// The underlying hierarchy item. + /// + public IVsHierarchyItem Item { get; } + + /// + /// The display name of the node + /// + public string Name { get; set; } + + /// + /// The absolute file path on disk. + /// + public string FileName { get; set; } + + /// + /// The type of node. + /// + public NodeType Type { get; } + + /// + /// A value indicating if the node can be expanded. + /// + public bool IsExpandable { get; } + + /// + /// A value indicating if the node is in a faulted state. + /// + public bool IsFaulted { get; } + + /// + /// A value indicating if the node is hidden. + /// + public bool IsHidden { get; } + + /// + /// A value indicating if the node is a root node. + /// + public bool IsRoot { get; } + + /// + /// A value indicating if the node is nested + /// + public bool IsNested { get; } + + /// + /// The parent node. Is when there is no parent. + /// + public ItemNode? Parent => _parent ??= Create(Item.Parent); + + /// + /// A list of child nodes. + /// + public IEnumerable Children => _children ??= Item.Children.Select(t => Create(t)); + + /// + /// Checks what kind the project is. + /// + /// Use the collection for known guids. + public bool IsKind(string typeGuid) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + if (Type == NodeType.Project || Type == NodeType.VirtualProject) + { + return _hierarchy.IsProjectOfType(typeGuid); + } + + return false; + } + + /// + /// Converts the node a . + /// + public EnvDTE.Project? ToProject() + { + return HierarchyUtilities.GetProject(Item); + } + + /// + /// Converts the node a . + /// + public EnvDTE.ProjectItem? ToProjectItem() + { + ThreadHelper.ThrowIfNotOnUIThread(); + + if (_hierarchy.TryGetItemProperty(_itemId, (int)__VSHPROPID.VSHPROPID_ExtObject, out object? obj)) + { + return obj as EnvDTE.ProjectItem; + } + + return null; + } + + /// + /// Creates a new instance based on a hierarchy. + /// + public static async Task CreateAsync(IVsHierarchy hierarchy, uint itemId) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + IVsHierarchyItem? item = await hierarchy.ToHierarcyItemAsync(itemId); + + return Create(item); + } + + /// + /// Creates a new instance based on a hierarchy item. + /// + public static ItemNode? Create(IVsHierarchyItem? item) + { + if (item == null) + { + return null; + } + + return new ItemNode(item); + } + + private NodeType GetNodeType(IVsHierarchyItemIdentity identity) + { + if (HierarchyUtilities.IsSolutionNode(identity)) + { + return NodeType.Solution; + } + else if (HierarchyUtilities.IsSolutionFolder(identity)) + { + return NodeType.SolutionFolder; + } + else if (HierarchyUtilities.IsMiscellaneousProject(identity)) + { + return NodeType.MiscProject; + } + else if (HierarchyUtilities.IsVirtualProject(identity)) + { + return NodeType.VirtualProject; + } + else if (HierarchyUtilities.IsProject(identity)) + { + return NodeType.Project; + } + else if (HierarchyUtilities.IsPhysicalFile(identity)) + { + return NodeType.PhysicalFile; + } + else if (HierarchyUtilities.IsPhysicalFolder(identity)) + { + return NodeType.PhysicalFolder; + } + + return NodeType.Unknown; + } + } + + /// + /// Types of nodes. + /// + public enum NodeType + { + /// Physical file on disk + PhysicalFile, + /// Physical folder on disk + PhysicalFolder, + /// A project + Project, + /// A miscellaneous project + MiscProject, + /// A virtual project + VirtualProject, + /// The solution + Solution, + /// A solution folder + SolutionFolder, + /// Unknown node type + Unknown, + } +} diff --git a/src/Community.VisualStudio.Toolkit.Shared/Services/Solution.cs b/src/Community.VisualStudio.Toolkit.Shared/Services/Solution.cs index 503ad73..5302582 100644 --- a/src/Community.VisualStudio.Toolkit.Shared/Services/Solution.cs +++ b/src/Community.VisualStudio.Toolkit.Shared/Services/Solution.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; using EnvDTE; @@ -83,6 +84,134 @@ public async Task> GetSelectedItemsAsync() return selectedObject as ProjectItem; } + /// + /// Gets the currently selected hierarchy items. + /// + public async Task> GetSelectedHierarchiesAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + IVsMonitorSelection? svc = await VS.GetRequiredServiceAsync(); + IntPtr hierPtr = IntPtr.Zero; + IntPtr containerPtr = IntPtr.Zero; + + List results = new(); + + try + { + svc.GetCurrentSelection(out hierPtr, out var itemId, out IVsMultiItemSelect multiSelect, out containerPtr); + + if (itemId == VSConstants.VSITEMID_SELECTION) + { + multiSelect.GetSelectionInfo(out var itemCount, out var fSingleHierarchy); + + var items = new VSITEMSELECTION[itemCount]; + multiSelect.GetSelectedItems(0, itemCount, items); + + foreach (VSITEMSELECTION item in items) + { + IVsHierarchyItem? hierItem = await item.pHier.ToHierarcyItemAsync(item.itemid); + if (hierItem != null && !results.Contains(hierItem)) + { + results.Add(hierItem); + } + } + } + else + { + if (hierPtr != IntPtr.Zero) + { + var hierarchy = (IVsHierarchy)Marshal.GetUniqueObjectForIUnknown(hierPtr); + IVsHierarchyItem? hierItem = await hierarchy.ToHierarcyItemAsync(itemId); + + if (hierItem != null) + { + results.Add(hierItem); + } + } + } + } + catch (Exception ex) + { + await ex.LogAsync(); + } + finally + { + if (hierPtr != IntPtr.Zero) + { + Marshal.Release(hierPtr); + } + + if (containerPtr != IntPtr.Zero) + { + Marshal.Release(containerPtr); + } + } + + return results; + } + + /// + /// Gets the currently selected nodes. + /// + public async Task> GetSelectedNodesAsync() + { + IEnumerable? hierarchies = await GetSelectedHierarchiesAsync(); + List nodes = new(); + + foreach (IVsHierarchyItem hierarchy in hierarchies) + { + ItemNode? node = await ItemNode.CreateAsync(hierarchy.HierarchyIdentity.Hierarchy, hierarchy.HierarchyIdentity.ItemID); + + if (node != null) + { + nodes.Add(node); + } + } + + return nodes; + } + + /// + /// Gets the currently selected node. + /// + public async Task GetActiveProjectNodeAsync() + { + IEnumerable? hierarchies = await GetSelectedHierarchiesAsync(); + IVsHierarchyItem? hierarchy = hierarchies.FirstOrDefault(); + + if (hierarchy != null) + { + return await ItemNode.CreateAsync(hierarchy.HierarchyIdentity.NestedHierarchy, VSConstants.VSITEMID_ROOT); + } + + return null; + } + + /// + /// Gets all projects in the solution + /// + public async Task> GetAllProjectNodesAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + IVsSolution solution = await GetSolutionAsync(); + IEnumerable? hierarchies = solution.GetAllProjectHierarchys(); + + List list = new(); + + foreach (IVsHierarchy? hierarchy in hierarchies) + { + ItemNode? proj = await ItemNode.CreateAsync(hierarchy, VSConstants.VSITEMID_ROOT); + + if (proj?.Type == NodeType.Project) + { + list.Add(proj); + } + } + + return list; + } + /// /// Gets the active project. /// diff --git a/src/Community.VisualStudio.Toolkit.Shared/VSSDK.Helpers.Shared.projitems b/src/Community.VisualStudio.Toolkit.Shared/VSSDK.Helpers.Shared.projitems index 2b874de..dd7ca05 100644 --- a/src/Community.VisualStudio.Toolkit.Shared/VSSDK.Helpers.Shared.projitems +++ b/src/Community.VisualStudio.Toolkit.Shared/VSSDK.Helpers.Shared.projitems @@ -39,6 +39,7 @@ + diff --git a/test/VSSDK.TestExtension/TestExtensionPackage.cs b/test/VSSDK.TestExtension/TestExtensionPackage.cs index ddb8a8c..0615e06 100644 --- a/test/VSSDK.TestExtension/TestExtensionPackage.cs +++ b/test/VSSDK.TestExtension/TestExtensionPackage.cs @@ -37,7 +37,6 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke await MultiInstanceWindowCommand.InitializeAsync(this); await BuildActiveProjectAsyncCommand.InitializeAsync(this); await BuildSolutionAsyncCommand.InitializeAsync(this); - } } } diff --git a/test/VSSDK.TestExtension/VSSDK.TestExtension.csproj b/test/VSSDK.TestExtension/VSSDK.TestExtension.csproj index c63efcf..bc91a2d 100644 --- a/test/VSSDK.TestExtension/VSSDK.TestExtension.csproj +++ b/test/VSSDK.TestExtension/VSSDK.TestExtension.csproj @@ -103,15 +103,12 @@ 16.10.1 - - 16.4.29519.181 - - 16.10.56 + 17.0.17-alpha runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -146,9 +143,9 @@ - - {38dc82ea-91d6-4360-b76c-995708ee43a3} - Community.VisualStudio.Toolkit.16.0 + + {d000c76d-4d0f-48f9-b5ab-5696c92cc7bd} + Community.VisualStudio.Toolkit.17.0