diff --git a/src/GitHub.App/ViewModels/GitHubPane/GitHubPaneViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/GitHubPaneViewModel.cs index 5693f26542..898e9f6962 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/GitHubPaneViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/GitHubPaneViewModel.cs @@ -49,8 +49,7 @@ public sealed class GitHubPaneViewModel : ViewModelBase, IGitHubPaneViewModel, I readonly ReactiveCommand refresh; readonly ReactiveCommand showPullRequests; readonly ReactiveCommand openInBrowser; - readonly SemaphoreSlim initializing = new SemaphoreSlim(1); - bool initialized; + Task initializeTask; IViewModel content; ILocalRepositoryModel localRepository; string searchQuery; @@ -201,39 +200,9 @@ public void Dispose() } /// - public async Task InitializeAsync(IServiceProvider paneServiceProvider) + public Task InitializeAsync(IServiceProvider paneServiceProvider) { - await initializing.WaitAsync(); - if (initialized) return; - - try - { - await UpdateContent(teamExplorerContext.ActiveRepository); - teamExplorerContext.WhenAnyValue(x => x.ActiveRepository) - .Skip(1) - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(x => UpdateContent(x).Forget()); - - connectionManager.Connections.CollectionChanged += (_, __) => UpdateContent(LocalRepository).Forget(); - - BindNavigatorCommand(paneServiceProvider, PkgCmdIDList.pullRequestCommand, showPullRequests); - BindNavigatorCommand(paneServiceProvider, PkgCmdIDList.backCommand, navigator.NavigateBack); - BindNavigatorCommand(paneServiceProvider, PkgCmdIDList.forwardCommand, navigator.NavigateForward); - BindNavigatorCommand(paneServiceProvider, PkgCmdIDList.refreshCommand, refresh); - BindNavigatorCommand(paneServiceProvider, PkgCmdIDList.githubCommand, openInBrowser); - - paneServiceProvider.AddCommandHandler(Guids.guidGitHubToolbarCmdSet, PkgCmdIDList.helpCommand, - (_, __) => - { - browser.OpenUrl(new Uri(GitHubUrls.Documentation)); - usageTracker.IncrementCounter(x => x.NumberOfGitHubPaneHelpClicks).Forget(); - }); - } - finally - { - initialized = true; - initializing.Release(); - } + return initializeTask = initializeTask ?? CreateInitializeTask(paneServiceProvider); } /// @@ -307,6 +276,30 @@ public Task ShowPullRequest(string owner, string repo, int number) x => x.RemoteRepositoryOwner == owner && x.LocalRepository.Name == repo && x.Number == number); } + async Task CreateInitializeTask(IServiceProvider paneServiceProvider) + { + await UpdateContent(teamExplorerContext.ActiveRepository); + teamExplorerContext.WhenAnyValue(x => x.ActiveRepository) + .Skip(1) + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(x => UpdateContent(x).Forget()); + + connectionManager.Connections.CollectionChanged += (_, __) => UpdateContent(LocalRepository).Forget(); + + BindNavigatorCommand(paneServiceProvider, PkgCmdIDList.pullRequestCommand, showPullRequests); + BindNavigatorCommand(paneServiceProvider, PkgCmdIDList.backCommand, navigator.NavigateBack); + BindNavigatorCommand(paneServiceProvider, PkgCmdIDList.forwardCommand, navigator.NavigateForward); + BindNavigatorCommand(paneServiceProvider, PkgCmdIDList.refreshCommand, refresh); + BindNavigatorCommand(paneServiceProvider, PkgCmdIDList.githubCommand, openInBrowser); + + paneServiceProvider.AddCommandHandler(Guids.guidGitHubToolbarCmdSet, PkgCmdIDList.helpCommand, + (_, __) => + { + browser.OpenUrl(new Uri(GitHubUrls.Documentation)); + usageTracker.IncrementCounter(x => x.NumberOfGitHubPaneHelpClicks).Forget(); + }); + } + OleMenuCommand BindNavigatorCommand(IServiceProvider paneServiceProvider, int commandId, ReactiveCommand command) { Guard.ArgumentNotNull(paneServiceProvider, nameof(paneServiceProvider)); diff --git a/src/GitHub.Exports/Models/UsageModel.cs b/src/GitHub.Exports/Models/UsageModel.cs index dcfd7ac34a..216619e55f 100644 --- a/src/GitHub.Exports/Models/UsageModel.cs +++ b/src/GitHub.Exports/Models/UsageModel.cs @@ -42,6 +42,7 @@ public struct UsageModel public int NumberOfPRDetailsNavigateToEditor { get; set; } public int NumberOfPRReviewDiffViewInlineCommentOpen { get; set; } public int NumberOfPRReviewDiffViewInlineCommentPost { get; set; } + public int NumberOfShowCurrentPullRequest { get; set; } public UsageModel Clone(bool includeWeekly, bool includeMonthly) { diff --git a/src/GitHub.Exports/Services/IVSUIContextFactory.cs b/src/GitHub.Exports/Services/IVSUIContextFactory.cs index e3aca6cbb7..57c6542d9b 100644 --- a/src/GitHub.Exports/Services/IVSUIContextFactory.cs +++ b/src/GitHub.Exports/Services/IVSUIContextFactory.cs @@ -20,6 +20,6 @@ public VSUIContextChangedEventArgs(bool activated) public interface IVSUIContext { bool IsActive { get; } - event EventHandler UIContextChanged; + void WhenActivated(Action action); } } \ No newline at end of file diff --git a/src/GitHub.Exports/Settings/Guids.cs b/src/GitHub.Exports/Settings/Guids.cs index 2e225c7038..81562cdaff 100644 --- a/src/GitHub.Exports/Settings/Guids.cs +++ b/src/GitHub.Exports/Settings/Guids.cs @@ -13,6 +13,7 @@ public static class Guids public const string StartPagePackageId = "3b764d23-faf7-486f-94c7-b3accc44a70e"; public const string CodeContainerProviderId = "6CE146CB-EF57-4F2C-A93F-5BA685317660"; public const string InlineReviewsPackageId = "248325BE-4A2D-4111-B122-E7D59BF73A35"; + public const string PullRequestStatusPackageId = "5121BEC6-1088-4553-8453-0DDC7C8E2238"; public const string TeamExplorerWelcomeMessage = "C529627F-8AA6-4FDB-82EB-4BFB7DB753C3"; public const string LoginManagerId = "7BA2071A-790A-4F95-BE4A-0EEAA5928AAF"; diff --git a/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj b/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj index 4fe7f00b32..418b70a6ae 100644 --- a/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj +++ b/src/GitHub.InlineReviews/GitHub.InlineReviews.csproj @@ -88,10 +88,12 @@ + + @@ -124,6 +126,7 @@ + DiffCommentThreadView.xaml @@ -161,6 +164,9 @@ CommentView.xaml + + PullRequestStatusView.xaml + @@ -443,6 +449,10 @@ MSBuild:Compile Designer + + Designer + MSBuild:Compile + diff --git a/src/GitHub.InlineReviews/InlineReviewsPackage.cs b/src/GitHub.InlineReviews/InlineReviewsPackage.cs index 9271fbd299..5d06a63d87 100644 --- a/src/GitHub.InlineReviews/InlineReviewsPackage.cs +++ b/src/GitHub.InlineReviews/InlineReviewsPackage.cs @@ -12,7 +12,7 @@ namespace GitHub.InlineReviews [Guid(Guids.InlineReviewsPackageId)] [ProvideAutoLoad(UIContextGuids80.SolutionExists)] [ProvideMenuResource("Menus.ctmenu", 1)] - [ProvideToolWindow(typeof(PullRequestCommentsPane), DocumentLikeTool=true)] + [ProvideToolWindow(typeof(PullRequestCommentsPane), DocumentLikeTool = true)] public class InlineReviewsPackage : Package { protected override void Initialize() diff --git a/src/GitHub.InlineReviews/PullRequestStatusBarPackage.cs b/src/GitHub.InlineReviews/PullRequestStatusBarPackage.cs new file mode 100644 index 0000000000..8b04edb866 --- /dev/null +++ b/src/GitHub.InlineReviews/PullRequestStatusBarPackage.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading; +using System.Runtime.InteropServices; +using GitHub.Services; +using GitHub.VisualStudio; +using GitHub.InlineReviews.Services; +using Microsoft.VisualStudio.Shell; +using Task = System.Threading.Tasks.Task; + +namespace GitHub.InlineReviews +{ + [Guid(Guids.PullRequestStatusPackageId)] + [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] + [ProvideAutoLoad(Guids.GitSccProviderId, PackageAutoLoadFlags.BackgroundLoad)] + public class PullRequestStatusBarPackage : AsyncPackage + { + /// + /// Initialize the PR status UI on Visual Studio's status bar. + /// + protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) + { + var usageTracker = (IUsageTracker)await GetServiceAsync(typeof(IUsageTracker)); + var serviceProvider = (IGitHubServiceProvider)await GetServiceAsync(typeof(IGitHubServiceProvider)); + var gitExt = (IVSGitExt)await GetServiceAsync(typeof(IVSGitExt)); + + new PullRequestStatusBarManager(gitExt, usageTracker, serviceProvider); + } + } +} diff --git a/src/GitHub.InlineReviews/Services/PullRequestStatusBarManager.cs b/src/GitHub.InlineReviews/Services/PullRequestStatusBarManager.cs new file mode 100644 index 0000000000..3ed151b856 --- /dev/null +++ b/src/GitHub.InlineReviews/Services/PullRequestStatusBarManager.cs @@ -0,0 +1,169 @@ +using System; +using System.Windows; +using System.Windows.Input; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.ComponentModel; +using System.ComponentModel.Composition; +using GitHub.InlineReviews.Views; +using GitHub.InlineReviews.ViewModels; +using GitHub.Services; +using GitHub.VisualStudio; +using GitHub.Models; +using GitHub.Logging; +using GitHub.Extensions; +using Serilog; + +namespace GitHub.InlineReviews.Services +{ + public class PullRequestStatusBarManager + { + static readonly ILogger log = LogManager.ForContext(); + const string StatusBarPartName = "PART_SccStatusBarHost"; + + readonly IVSGitExt gitExt; + readonly IUsageTracker usageTracker; + readonly IGitHubServiceProvider serviceProvider; + + IPullRequestSessionManager pullRequestSessionManager; + + [ImportingConstructor] + public PullRequestStatusBarManager(IVSGitExt gitExt, IUsageTracker usageTracker, IGitHubServiceProvider serviceProvider) + { + this.gitExt = gitExt; + this.usageTracker = usageTracker; + this.serviceProvider = serviceProvider; + + OnActiveRepositoriesChanged(); + gitExt.ActiveRepositoriesChanged += OnActiveRepositoriesChanged; + } + + void OnActiveRepositoriesChanged() + { + if (gitExt.ActiveRepositories.Count > 0) + { + gitExt.ActiveRepositoriesChanged -= OnActiveRepositoriesChanged; + Application.Current.Dispatcher.Invoke(() => StartShowingStatus()); + } + } + + void StartShowingStatus() + { + try + { + // Create just in time on Main thread. + pullRequestSessionManager = serviceProvider.GetService(); + + RefreshCurrentSession(); + pullRequestSessionManager.PropertyChanged += PullRequestSessionManager_PropertyChanged; + } + catch (Exception e) + { + log.Error(e, "Error initializing"); + } + } + + void PullRequestSessionManager_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(PullRequestSessionManager.CurrentSession)) + { + RefreshCurrentSession(); + } + } + + void RefreshCurrentSession() + { + var pullRequest = pullRequestSessionManager.CurrentSession?.PullRequest; + var viewModel = pullRequest != null ? CreatePullRequestStatusViewModel(pullRequest) : null; + ShowStatus(viewModel); + } + + PullRequestStatusViewModel CreatePullRequestStatusViewModel(IPullRequestModel pullRequest) + { + var dte = serviceProvider.TryGetService(); + var command = new RaisePullRequestCommand(dte, usageTracker); + var pullRequestStatusViewModel = new PullRequestStatusViewModel(command); + pullRequestStatusViewModel.Number = pullRequest.Number; + pullRequestStatusViewModel.Title = pullRequest.Title; + return pullRequestStatusViewModel; + } + + void ShowStatus(PullRequestStatusViewModel pullRequestStatusViewModel = null) + { + var statusBar = FindSccStatusBar(Application.Current.MainWindow); + if (statusBar != null) + { + var githubStatusBar = Find(statusBar); + if (githubStatusBar != null) + { + // Replace to ensure status shows up. + statusBar.Items.Remove(githubStatusBar); + } + + if (pullRequestStatusViewModel != null) + { + githubStatusBar = new PullRequestStatusView { DataContext = pullRequestStatusViewModel }; + statusBar.Items.Insert(0, githubStatusBar); + } + } + } + + static T Find(StatusBar statusBar) + { + foreach (var item in statusBar.Items) + { + if (item is T) + { + return (T)item; + } + } + + return default(T); + } + + StatusBar FindSccStatusBar(Window mainWindow) + { + var contentControl = mainWindow?.Template?.FindName(StatusBarPartName, mainWindow) as ContentControl; + return contentControl?.Content as StatusBar; + } + + class RaisePullRequestCommand : ICommand + { + readonly string guid = Guids.guidGitHubCmdSetString; + readonly int id = PkgCmdIDList.showCurrentPullRequestCommand; + + readonly EnvDTE.DTE dte; + readonly IUsageTracker usageTracker; + + internal RaisePullRequestCommand(EnvDTE.DTE dte, IUsageTracker usageTracker) + { + this.dte = dte; + this.usageTracker = usageTracker; + } + + public bool CanExecute(object parameter) => true; + + public void Execute(object parameter) + { + try + { + object customIn = null; + object customOut = null; + dte?.Commands.Raise(guid, id, ref customIn, ref customOut); + } + catch (Exception e) + { + log.Error(e, "Couldn't raise {Guid}:{ID}", guid, id); + } + + usageTracker.IncrementCounter(x => x.NumberOfShowCurrentPullRequest).Forget(); + } + + public event EventHandler CanExecuteChanged + { + add { } + remove { } + } + } + } +} diff --git a/src/GitHub.InlineReviews/ViewModels/PullRequestStatusViewModel.cs b/src/GitHub.InlineReviews/ViewModels/PullRequestStatusViewModel.cs new file mode 100644 index 0000000000..ee7b1e285e --- /dev/null +++ b/src/GitHub.InlineReviews/ViewModels/PullRequestStatusViewModel.cs @@ -0,0 +1,47 @@ +using System; +using System.Windows.Input; +using System.ComponentModel; + +namespace GitHub.InlineReviews.ViewModels +{ + public class PullRequestStatusViewModel : INotifyPropertyChanged + { + int? number; + string title; + + public PullRequestStatusViewModel(ICommand showCurrentPullRequestCommand) + { + ShowCurrentPullRequestCommand = showCurrentPullRequestCommand; + } + + public int? Number + { + get { return number; } + set + { + if (number != value) + { + number = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Number))); + } + } + } + + public string Title + { + get { return title; } + set + { + if (title != value) + { + title = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Title))); + } + } + } + + public ICommand ShowCurrentPullRequestCommand { get; } + + public event PropertyChangedEventHandler PropertyChanged; + } +} diff --git a/src/GitHub.InlineReviews/Views/PullRequestStatusView.xaml b/src/GitHub.InlineReviews/Views/PullRequestStatusView.xaml new file mode 100644 index 0000000000..1426cc6b5c --- /dev/null +++ b/src/GitHub.InlineReviews/Views/PullRequestStatusView.xaml @@ -0,0 +1,63 @@ + + + + + + + + + + + # - + + + + diff --git a/src/GitHub.InlineReviews/Views/PullRequestStatusView.xaml.cs b/src/GitHub.InlineReviews/Views/PullRequestStatusView.xaml.cs new file mode 100644 index 0000000000..535830924a --- /dev/null +++ b/src/GitHub.InlineReviews/Views/PullRequestStatusView.xaml.cs @@ -0,0 +1,13 @@ +using System; +using System.Windows.Controls; + +namespace GitHub.InlineReviews.Views +{ + public partial class PullRequestStatusView : UserControl + { + public PullRequestStatusView() + { + InitializeComponent(); + } + } +} diff --git a/src/GitHub.TeamFoundation.14/GitHub.TeamFoundation.14.csproj b/src/GitHub.TeamFoundation.14/GitHub.TeamFoundation.14.csproj index 352f4c7b3b..edd4dcd07a 100644 --- a/src/GitHub.TeamFoundation.14/GitHub.TeamFoundation.14.csproj +++ b/src/GitHub.TeamFoundation.14/GitHub.TeamFoundation.14.csproj @@ -79,6 +79,10 @@ ..\..\packages\Microsoft.VisualStudio.ComponentModelHost.14.0.25424\lib\net45\Microsoft.VisualStudio.ComponentModelHost.dll True + + ..\..\packages\Microsoft.VisualStudio.OLE.Interop.7.10.6070\lib\Microsoft.VisualStudio.OLE.Interop.dll + True + ..\..\packages\Microsoft.VisualStudio.Shell.14.0.14.3.25407\lib\Microsoft.VisualStudio.Shell.14.0.dll True @@ -87,6 +91,15 @@ ..\..\packages\Microsoft.VisualStudio.Shell.Immutable.10.0.10.0.30319\lib\net40\Microsoft.VisualStudio.Shell.Immutable.10.0.dll True + + ..\..\packages\Microsoft.VisualStudio.Shell.Immutable.11.0.11.0.50727\lib\net45\Microsoft.VisualStudio.Shell.Immutable.11.0.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Shell.Immutable.14.0.14.3.25407\lib\net45\Microsoft.VisualStudio.Shell.Immutable.14.0.dll + True + global + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.7.10.6071\lib\Microsoft.VisualStudio.Shell.Interop.dll True @@ -95,6 +108,10 @@ ..\..\packages\Microsoft.VisualStudio.Shell.Interop.8.0.8.0.50727\lib\Microsoft.VisualStudio.Shell.Interop.8.0.dll True + + ..\..\packages\Microsoft.VisualStudio.TextManager.Interop.7.10.6070\lib\Microsoft.VisualStudio.TextManager.Interop.dll + True + diff --git a/src/GitHub.TeamFoundation.14/Services/VSGitExt.cs b/src/GitHub.TeamFoundation.14/Services/VSGitExt.cs index 6dd1d5fd12..0ddc13eba3 100644 --- a/src/GitHub.TeamFoundation.14/Services/VSGitExt.cs +++ b/src/GitHub.TeamFoundation.14/Services/VSGitExt.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Threading.Tasks; using System.Collections.Generic; using System.ComponentModel.Composition; using GitHub.Models; @@ -7,6 +8,7 @@ using GitHub.Logging; using GitHub.TeamFoundation.Services; using Serilog; +using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.TeamFoundation.Git.Extensibility; using Task = System.Threading.Tasks.Task; @@ -15,14 +17,15 @@ namespace GitHub.VisualStudio.Base /// /// This service acts as an always available version of . /// - [Export(typeof(IVSGitExt))] - [PartCreationPolicy(CreationPolicy.Shared)] + /// + /// Initialization for this service will be done asynchronously and the service will be + /// retrieved using . This means the service can be constructed and subscribed to from a background thread. + /// public class VSGitExt : IVSGitExt { static readonly ILogger log = LogManager.ForContext(); - readonly IGitHubServiceProvider serviceProvider; - readonly IVSUIContext context; + readonly IAsyncServiceProvider asyncServiceProvider; readonly ILocalRepositoryModelFactory repositoryFactory; readonly object refreshLock = new object(); @@ -30,55 +33,37 @@ public class VSGitExt : IVSGitExt IReadOnlyList activeRepositories; [ImportingConstructor] - public VSGitExt(IGitHubServiceProvider serviceProvider) - : this(serviceProvider, new VSUIContextFactory(), new LocalRepositoryModelFactory()) + public VSGitExt(IAsyncServiceProvider asyncServiceProvider) + : this(asyncServiceProvider, new VSUIContextFactory(), new LocalRepositoryModelFactory()) { } - public VSGitExt(IGitHubServiceProvider serviceProvider, IVSUIContextFactory factory, ILocalRepositoryModelFactory repositoryFactory) + public VSGitExt(IAsyncServiceProvider asyncServiceProvider, IVSUIContextFactory factory, ILocalRepositoryModelFactory repositoryFactory) { - this.serviceProvider = serviceProvider; + this.asyncServiceProvider = asyncServiceProvider; this.repositoryFactory = repositoryFactory; - // The IGitExt service isn't available when a TFS based solution is opened directly. - // It will become available when moving to a Git based solution (cause a UIContext event to fire). - context = factory.GetUIContext(new Guid(Guids.GitSccProviderId)); - - // Start with empty array until we have a change to initialize. + // Start with empty array until we have a chance to initialize. ActiveRepositories = Array.Empty(); - if (context.IsActive && TryInitialize()) - { - // Refresh ActiveRepositories on background thread so we don't delay startup. - InitializeTask = Task.Run(() => RefreshActiveRepositories()); - } - else - { - // If we're not in the UIContext or TryInitialize fails, have another go when the UIContext changes. - context.UIContextChanged += ContextChanged; - log.Debug("VSGitExt will be initialized later"); - InitializeTask = Task.CompletedTask; - } + // The IGitExt service isn't available when a TFS based solution is opened directly. + // It will become available when moving to a Git based solution (and cause a UIContext event to fire). + var context = factory.GetUIContext(new Guid(Guids.GitSccProviderId)); + context.WhenActivated(() => Initialize()); } - void ContextChanged(object sender, VSUIContextChangedEventArgs e) + void Initialize() { - // If we're in the UIContext and TryInitialize succeeds, we can stop listening for events. - // NOTE: this event can fire with UIContext=true in a TFS solution (not just Git). - if (e.Activated && TryInitialize()) + PendingTasks = asyncServiceProvider.GetServiceAsync(typeof(IGitExt)).ContinueWith(t => { - // Refresh ActiveRepositories on background thread so we don't delay UI context change. - InitializeTask = Task.Run(() => RefreshActiveRepositories()); - context.UIContextChanged -= ContextChanged; - log.Debug("Initialized VSGitExt on UIContextChanged"); - } - } + gitService = (IGitExt)t.Result; + if (gitService == null) + { + log.Error("Couldn't find IGitExt service"); + return; + } - bool TryInitialize() - { - gitService = serviceProvider.GetService(); - if (gitService != null) - { + RefreshActiveRepositories(); gitService.PropertyChanged += (s, e) => { if (e.PropertyName == nameof(gitService.ActiveRepositories)) @@ -86,13 +71,7 @@ bool TryInitialize() RefreshActiveRepositories(); } }; - - log.Debug("Found IGitExt service and initialized VSGitExt"); - return true; - } - - log.Error("Couldn't find IGitExt service"); - return false; + }, TaskScheduler.Default); } void RefreshActiveRepositories() @@ -104,7 +83,7 @@ void RefreshActiveRepositories() log.Debug( "IGitExt.ActiveRepositories (#{Id}) returned {Repositories}", gitService.GetHashCode(), - gitService?.ActiveRepositories.Select(x => x.RepositoryPath)); + gitService.ActiveRepositories.Select(x => x.RepositoryPath)); ActiveRepositories = gitService?.ActiveRepositories.Select(x => repositoryFactory.Create(x.RepositoryPath)).ToList(); } @@ -136,6 +115,9 @@ private set public event Action ActiveRepositoriesChanged; - public Task InitializeTask { get; private set; } + /// + /// Tasks that are pending execution on the thread pool. + /// + public Task PendingTasks { get; private set; } = Task.CompletedTask; } } \ No newline at end of file diff --git a/src/GitHub.TeamFoundation.14/Services/VSGitServices.cs b/src/GitHub.TeamFoundation.14/Services/VSGitServices.cs index 59b341f440..db973edd2d 100644 --- a/src/GitHub.TeamFoundation.14/Services/VSGitServices.cs +++ b/src/GitHub.TeamFoundation.14/Services/VSGitServices.cs @@ -1,4 +1,10 @@ -using System; +#if TEAMEXPLORER15 +// Microsoft.VisualStudio.Shell.Framework has an alias to avoid conflict with IAsyncServiceProvider +extern alias SF15; +using ServiceProgressData = SF15::Microsoft.VisualStudio.Shell.ServiceProgressData; +#endif + +using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.Globalization; @@ -84,8 +90,7 @@ public async Task Clone( await gitExt.WhenAnyValue(x => x.CanClone).Where(x => x).Take(1); #else var gitExt = serviceProvider.GetService(); - var typedProgress = ((Progress)progress) ?? - new Progress(); + var typedProgress = ((Progress)progress) ?? new Progress(); await Microsoft.VisualStudio.Shell.ThreadHelper.JoinableTaskFactory.RunAsync(async () => { diff --git a/src/GitHub.TeamFoundation.14/Services/VSUIContextFactory.cs b/src/GitHub.TeamFoundation.14/Services/VSUIContextFactory.cs index 6cedf85cd8..3797f75c02 100644 --- a/src/GitHub.TeamFoundation.14/Services/VSUIContextFactory.cs +++ b/src/GitHub.TeamFoundation.14/Services/VSUIContextFactory.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.ComponentModel.Composition; using Microsoft.VisualStudio.Shell; using GitHub.Services; @@ -17,8 +15,7 @@ public IVSUIContext GetUIContext(Guid contextGuid) class VSUIContext : IVSUIContext { readonly UIContext context; - readonly Dictionary, EventHandler> handlers = - new Dictionary, EventHandler>(); + public VSUIContext(UIContext context) { this.context = context; @@ -26,27 +23,6 @@ public VSUIContext(UIContext context) public bool IsActive { get { return context.IsActive; } } - public event EventHandler UIContextChanged - { - add - { - EventHandler handler = null; - if (!handlers.TryGetValue(value, out handler)) - { - handler = (s, e) => value.Invoke(s, new VSUIContextChangedEventArgs(e.Activated)); - handlers.Add(value, handler); - } - context.UIContextChanged += handler; - } - remove - { - EventHandler handler = null; - if (handlers.TryGetValue(value, out handler)) - { - handlers.Remove(value); - context.UIContextChanged -= handler; - } - } - } + public void WhenActivated(Action action) => context.WhenActivated(action); } } diff --git a/src/GitHub.TeamFoundation.14/packages.config b/src/GitHub.TeamFoundation.14/packages.config index acb5ea18a9..1dd7aa0c9b 100644 --- a/src/GitHub.TeamFoundation.14/packages.config +++ b/src/GitHub.TeamFoundation.14/packages.config @@ -3,10 +3,14 @@ + + + + diff --git a/src/GitHub.TeamFoundation.15/GitHub.TeamFoundation.15.csproj b/src/GitHub.TeamFoundation.15/GitHub.TeamFoundation.15.csproj index 1f24f6c7bb..059045544e 100644 --- a/src/GitHub.TeamFoundation.15/GitHub.TeamFoundation.15.csproj +++ b/src/GitHub.TeamFoundation.15/GitHub.TeamFoundation.15.csproj @@ -94,6 +94,16 @@ ..\..\packages\Microsoft.VisualStudio.Shell.Framework.15.4.27004\lib\net45\Microsoft.VisualStudio.Shell.Framework.dll True + SF15 + + + ..\..\packages\Microsoft.VisualStudio.Shell.Immutable.11.0.11.0.50727\lib\net45\Microsoft.VisualStudio.Shell.Immutable.11.0.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Shell.Immutable.14.0.14.3.25407\lib\net45\Microsoft.VisualStudio.Shell.Immutable.14.0.dll + True + global ..\..\packages\Microsoft.VisualStudio.Shell.Interop.7.10.6071\lib\Microsoft.VisualStudio.Shell.Interop.dll diff --git a/src/GitHub.TeamFoundation.15/packages.config b/src/GitHub.TeamFoundation.15/packages.config index fa4276e3e1..ca6ba30df0 100644 --- a/src/GitHub.TeamFoundation.15/packages.config +++ b/src/GitHub.TeamFoundation.15/packages.config @@ -8,6 +8,8 @@ + + diff --git a/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj b/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj index 18f6d03a44..056d46301a 100644 --- a/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj +++ b/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj @@ -328,6 +328,7 @@ + @@ -679,6 +680,7 @@ True BuiltProjectOutputGroup;GetCopyToOutputDirectoryItems;DebugSymbolsProjectOutputGroup; DebugSymbolsProjectOutputGroup; + TF14 {161dbf01-1dbf-4b00-8551-c5c00f26720e} @@ -686,6 +688,7 @@ True BuiltProjectOutputGroup;GetCopyToOutputDirectoryItems;DebugSymbolsProjectOutputGroup; DebugSymbolsProjectOutputGroup; + TF15 {158b05e8-fdbc-4d71-b871-c96e28d5adf5} diff --git a/src/GitHub.VisualStudio/GitHubPackage.cs b/src/GitHub.VisualStudio/GitHubPackage.cs index 3066879b5e..2c1515caed 100644 --- a/src/GitHub.VisualStudio/GitHubPackage.cs +++ b/src/GitHub.VisualStudio/GitHubPackage.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel.Composition; using System.Diagnostics; -using System.Reflection; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -113,6 +112,7 @@ public GHClient(IProgram program) [ProvideService(typeof(IGitHubServiceProvider), IsAsyncQueryable = true)] [ProvideService(typeof(IUsageTracker), IsAsyncQueryable = true)] [ProvideService(typeof(IUsageService), IsAsyncQueryable = true)] + [ProvideService(typeof(IVSGitExt), IsAsyncQueryable = true)] [ProvideService(typeof(IGitHubToolWindowManager))] [Guid(ServiceProviderPackageId)] public sealed class ServiceProviderPackage : AsyncPackage, IServiceProviderPackage, IGitHubToolWindowManager @@ -150,6 +150,7 @@ Version VSVersion protected override Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) { AddService(typeof(IGitHubServiceProvider), CreateService, true); + AddService(typeof(IVSGitExt), CreateService, true); AddService(typeof(IUsageTracker), CreateService, true); AddService(typeof(IUsageService), CreateService, true); AddService(typeof(ILoginManager), CreateService, true); @@ -259,6 +260,11 @@ async Task CreateService(IAsyncServiceContainer container, CancellationT var serviceProvider = await GetServiceAsync(typeof(IGitHubServiceProvider)) as IGitHubServiceProvider; return new UsageTracker(serviceProvider, usageService); } + else if (serviceType == typeof(IVSGitExt)) + { + var vsVersion = ApplicationInfo.GetHostVersionInfo().FileMajorPart; + return VSGitExtFactory.Create(vsVersion, this); + } else if (serviceType == typeof(IGitHubToolWindowManager)) { return this; diff --git a/src/GitHub.VisualStudio/Services/VSGitExtFactory.cs b/src/GitHub.VisualStudio/Services/VSGitExtFactory.cs new file mode 100644 index 0000000000..6bcfe8f631 --- /dev/null +++ b/src/GitHub.VisualStudio/Services/VSGitExtFactory.cs @@ -0,0 +1,47 @@ +extern alias TF14; +extern alias TF15; + +using System; +using System.ComponentModel.Composition; +using GitHub.Info; +using GitHub.Logging; +using Serilog; +using Microsoft.VisualStudio.Shell; +using VSGitExt14 = TF14.GitHub.VisualStudio.Base.VSGitExt; +using VSGitExt15 = TF15.GitHub.VisualStudio.Base.VSGitExt; + +namespace GitHub.Services +{ + [PartCreationPolicy(CreationPolicy.Shared)] + public class VSGitExtFactory + { + static readonly ILogger log = LogManager.ForContext(); + + [ImportingConstructor] + public VSGitExtFactory(IGitHubServiceProvider serviceProvider) + { + VSGitExt = serviceProvider.GetService(); + } + + public static IVSGitExt Create(int vsVersion, IAsyncServiceProvider sp) + { + switch (vsVersion) + { + case 14: + return Create(() => new VSGitExt14(sp)); + case 15: + return Create(() => new VSGitExt15(sp)); + default: + log.Error("There is no IVSGitExt implementation for DTE version {Version}", vsVersion); + return null; + } + } + + // NOTE: We're being careful to only reference VSGitExt14 and VSGitExt15 from inside a lambda expression. + // This ensures that only the type that's compatible with the running DTE version is loaded. + static IVSGitExt Create(Func factory) => factory.Invoke(); + + [Export] + public IVSGitExt VSGitExt { get; } + } +} diff --git a/test/UnitTests/GitHub.TeamFoundation/VSGitExtTests.cs b/test/UnitTests/GitHub.TeamFoundation/VSGitExtTests.cs index 153d6295fc..c7304ad2f3 100644 --- a/test/UnitTests/GitHub.TeamFoundation/VSGitExtTests.cs +++ b/test/UnitTests/GitHub.TeamFoundation/VSGitExtTests.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading; using System.ComponentModel; using System.Collections.Generic; @@ -9,24 +10,23 @@ using NUnit.Framework; using NSubstitute; using Microsoft.VisualStudio.TeamFoundation.Git.Extensibility; -using System.Threading.Tasks; -using System.Linq; -using GitHub.TeamFoundation.Services; +using Microsoft.VisualStudio.Shell; +using Task = System.Threading.Tasks.Task; public class VSGitExtTests { - public class TheConstructor + public class TheConstructor : TestBaseClass { [TestCase(true, 1)] [TestCase(false, 0)] public void GetServiceIGitExt_WhenSccProviderContextIsActive(bool isActive, int expectCalls) { var context = CreateVSUIContext(isActive); - var sp = Substitute.For(); + var sp = Substitute.For(); var target = CreateVSGitExt(context, sp: sp); - sp.Received(expectCalls).GetService(); + sp.Received(expectCalls).GetServiceAsync(typeof(IGitExt)); } [TestCase(true, 1)] @@ -34,13 +34,13 @@ public void GetServiceIGitExt_WhenSccProviderContextIsActive(bool isActive, int public void GetServiceIGitExt_WhenUIContextChanged(bool activated, int expectCalls) { var context = CreateVSUIContext(false); - var sp = Substitute.For(); + var sp = Substitute.For(); var target = CreateVSGitExt(context, sp: sp); - var eventArgs = new VSUIContextChangedEventArgs(activated); - context.UIContextChanged += Raise.Event>(context, eventArgs); + context.IsActive = activated; - sp.Received(expectCalls).GetService(); + target.PendingTasks.Wait(); + sp.Received(expectCalls).GetServiceAsync(typeof(IGitExt)); } [Test] @@ -60,10 +60,10 @@ public void ActiveRepositories_ReadUsingThreadPoolThread() } } - public class TheActiveRepositoriesChangedEvent + public class TheActiveRepositoriesChangedEvent : TestBaseClass { [Test] - public void GitExtPropertyChangedEvent_ActiveRepositoriesChangedIsFired() + public async Task GitExtPropertyChangedEvent_ActiveRepositoriesChangedIsFired() { var context = CreateVSUIContext(true); var gitExt = CreateGitExt(); @@ -75,6 +75,7 @@ public void GitExtPropertyChangedEvent_ActiveRepositoriesChangedIsFired() var eventArgs = new PropertyChangedEventArgs(nameof(gitExt.ActiveRepositories)); gitExt.PropertyChanged += Raise.Event(gitExt, eventArgs); + await target.PendingTasks; Assert.That(wasFired, Is.True); } @@ -106,9 +107,8 @@ public void WhenUIContextChanged_ActiveRepositoriesChangedIsFired() bool wasFired = false; target.ActiveRepositoriesChanged += () => wasFired = true; - var eventArgs = new VSUIContextChangedEventArgs(true); - context.UIContextChanged += Raise.Event>(context, eventArgs); - target.InitializeTask.Wait(); + context.IsActive = true; + target.PendingTasks.Wait(); Assert.That(wasFired, Is.True); } @@ -123,15 +123,14 @@ public void WhenUIContextChanged_FiredUsingThreadPoolThread() bool threadPool = false; target.ActiveRepositoriesChanged += () => threadPool = Thread.CurrentThread.IsThreadPoolThread; - var eventArgs = new VSUIContextChangedEventArgs(true); - context.UIContextChanged += Raise.Event>(context, eventArgs); - target.InitializeTask.Wait(); + context.IsActive = true; + target.PendingTasks.Wait(); Assert.That(threadPool, Is.True); } } - public class TheActiveRepositoriesProperty + public class TheActiveRepositoriesProperty : TestBaseClass { [Test] public void SccProviderContextNotActive_IsEmpty() @@ -150,7 +149,7 @@ public void SccProviderContextIsActive_InitializeWithActiveRepositories() var context = CreateVSUIContext(true); var gitExt = CreateGitExt(new[] { repoPath }); var target = CreateVSGitExt(context, gitExt, repoFactory: repoFactory); - target.InitializeTask.Wait(); + target.PendingTasks.Wait(); var activeRepositories = target.ActiveRepositories; @@ -167,7 +166,7 @@ public void ExceptionRefreshingRepositories_ReturnsEmptyList() var context = CreateVSUIContext(true); var gitExt = CreateGitExt(new[] { repoPath }); var target = CreateVSGitExt(context, gitExt, repoFactory: repoFactory); - target.InitializeTask.Wait(); + target.PendingTasks.Wait(); var activeRepositories = target.ActiveRepositories; @@ -188,6 +187,7 @@ public async Task ActiveRepositoriesChangedOrderingShouldBeCorrectAcrossThreads( var task2 = Task.Run(() => gitExt.ActiveRepositories = activeRepositories2); await Task.WhenAll(task1, task2); + await target.PendingTasks; Assert.That(target.ActiveRepositories.Single().LocalPath, Is.EqualTo("repo2")); } @@ -206,20 +206,19 @@ static IReadOnlyList CreateActiveRepositories(params string[ return repositories.AsReadOnly(); } - static VSGitExt CreateVSGitExt(IVSUIContext context = null, IGitExt gitExt = null, IGitHubServiceProvider sp = null, + static VSGitExt CreateVSGitExt(IVSUIContext context = null, IGitExt gitExt = null, IAsyncServiceProvider sp = null, ILocalRepositoryModelFactory repoFactory = null) { context = context ?? CreateVSUIContext(true); gitExt = gitExt ?? CreateGitExt(); - sp = sp ?? Substitute.For(); + sp = sp ?? Substitute.For(); repoFactory = repoFactory ?? Substitute.For(); var factory = Substitute.For(); var contextGuid = new Guid(Guids.GitSccProviderId); factory.GetUIContext(contextGuid).Returns(context); - sp.GetService().Returns(factory); - sp.GetService().Returns(gitExt); + sp.GetServiceAsync(typeof(IGitExt)).Returns(gitExt); var vsGitExt = new VSGitExt(sp, factory, repoFactory); - vsGitExt.InitializeTask.Wait(); + vsGitExt.PendingTasks.Wait(); return vsGitExt; } @@ -231,11 +230,40 @@ static IGitExt CreateGitExt(params string[] repositoryPaths) return gitExt; } - static IVSUIContext CreateVSUIContext(bool isActive) + static MockVSUIContext CreateVSUIContext(bool isActive) + { + return new MockVSUIContext { IsActive = isActive }; + } + + class MockVSUIContext : IVSUIContext { - var context = Substitute.For(); - context.IsActive.Returns(isActive); - return context; + bool isActive; + Action action; + + public bool IsActive + { + get { return isActive; } + set + { + isActive = value; + if (isActive && action != null) + { + action.Invoke(); + action = null; + } + } + } + + public void WhenActivated(Action action) + { + if (isActive) + { + action.Invoke(); + return; + } + + this.action = action; + } } class MockGitExt : IGitExt diff --git a/test/UnitTests/UnitTests.csproj b/test/UnitTests/UnitTests.csproj index e84c3801bb..3c26b01676 100644 --- a/test/UnitTests/UnitTests.csproj +++ b/test/UnitTests/UnitTests.csproj @@ -107,6 +107,10 @@ ..\..\packages\Microsoft.VisualStudio.Shell.Immutable.12.0.12.0.21003\lib\net45\Microsoft.VisualStudio.Shell.Immutable.12.0.dll True + + ..\..\packages\Microsoft.VisualStudio.Shell.Immutable.14.0.14.3.25407\lib\net45\Microsoft.VisualStudio.Shell.Immutable.14.0.dll + True + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.7.10.6071\lib\Microsoft.VisualStudio.Shell.Interop.dll True diff --git a/test/UnitTests/packages.config b/test/UnitTests/packages.config index fec6f8280f..a80e0e8c78 100644 --- a/test/UnitTests/packages.config +++ b/test/UnitTests/packages.config @@ -12,6 +12,7 @@ +