diff --git a/.gitignore b/.gitignore index 7660f42117..c7c50193b6 100644 --- a/.gitignore +++ b/.gitignore @@ -239,4 +239,3 @@ WiX.Toolset.DummyFile.txt nunit-UnitTests.xml nunit-TrackingCollectionTests.xml GitHubVS.sln.DotSettings -**/generated/*.cs diff --git a/.gitmodules b/.gitmodules index c8fcb1dca5..4609f2bfa2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "submodules/rothko"] path = submodules/rothko - url = https://github.com/Haacked/Rothko.git + url = https://github.com/editor-tools/Rothko.git [submodule "submodules/reactiveui"] path = submodules/reactiveui url = https://github.com/shana/ReactiveUI diff --git a/GitHubVS.sln b/GitHubVS.sln index 26f9a22363..e4a23a01e8 100644 --- a/GitHubVS.sln +++ b/GitHubVS.sln @@ -1,9 +1,12 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.VisualStudio", "src\GitHub.VisualStudio\GitHub.VisualStudio.csproj", "{11569514-5AE5-4B5B-92A2-F10B0967DE5F}" + ProjectSection(ProjectDependencies) = postProject + {1BC94B6A-B021-4207-A70E-936EE272AD3E} = {1BC94B6A-B021-4207-A70E-936EE272AD3E} + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Meta", "Meta", "{72036B62-2FA6-4A22-8B33-69F698A18CF1}" ProjectSection(SolutionItems) = preProject @@ -31,7 +34,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{8E1F script\Modules\BuildUtils.psm1 = script\Modules\BuildUtils.psm1 script\Modules\Debugging.psm1 = script\Modules\Debugging.psm1 script\Modules\Vsix.psm1 = script\Modules\Vsix.psm1 - script\Modules\WiX.psm1 = script\Modules\WiX.psm1 EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Script", "Script", "{7B6C5F8D-14B3-443D-B044-0E209AE12BDF}" @@ -67,8 +69,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Exports.Reactive", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DesignTimeStyleHelper", "src\DesignTimeStyleHelper\DesignTimeStyleHelper.csproj", "{B1F5C227-456F-437D-BD5F-4C11B7A8D1A0}" EndProject -Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "MsiInstaller", "src\MsiInstaller\MsiInstaller.wixproj", "{1ED83084-2A57-4F89-915C-8A2167C0D6BC}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Octokit", "Octokit", "{1E7F7253-A6AF-43C4-A955-37BEDDA01AC0}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Octokit", "submodules\octokit.net\Octokit\Octokit.csproj", "{08DD4305-7787-4823-A53F-4D0F725A07F3}" @@ -81,8 +81,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveUI_Net45", "submodu EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveUI.Events_Net45", "submodules\reactiveui\ReactiveUI.Events\ReactiveUI.Events_Net45.csproj", "{600998C4-54DD-4755-BFA8-6F44544D8E2E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventBuilder", "submodules\reactiveui\ReactiveUI.Events\EventBuilder.csproj", "{3D4AE5F9-A535-4D5C-8F30-1A35D7BA0A3D}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Akavache", "Akavache", "{1E7F7253-A6AF-43C4-A955-37BEDDA01AC9}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akavache_Net45", "submodules\akavache\Akavache\Akavache_Net45.csproj", "{B4E665E5-6CAF-4414-A6E2-8DE1C3BCF203}" @@ -109,6 +107,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.TeamFoundation.15", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.VisualStudio.UI", "src\GitHub.VisualStudio.UI\GitHub.VisualStudio.UI.csproj", "{D1DFBB0C-B570-4302-8F1E-2E3A19C41961}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Subpackages", "Subpackages", "{7F25BDD5-474F-4EC1-A624-ED946B91F34E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.StartPage.Preview5", "submodules\externalpackages\StartPage\preview5\GitHub.StartPage.Preview5.csproj", "{1BC94B6A-B021-4207-A70E-936EE272AD3E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -301,19 +303,6 @@ Global {B1F5C227-456F-437D-BD5F-4C11B7A8D1A0}.XamlDesign|Any CPU.Build.0 = Debug|Any CPU {B1F5C227-456F-437D-BD5F-4C11B7A8D1A0}.XamlDesign|x86.ActiveCfg = Debug|Any CPU {B1F5C227-456F-437D-BD5F-4C11B7A8D1A0}.XamlDesign|x86.Build.0 = Debug|Any CPU - {1ED83084-2A57-4F89-915C-8A2167C0D6BC}.Debug|Any CPU.ActiveCfg = Debug|x86 - {1ED83084-2A57-4F89-915C-8A2167C0D6BC}.Debug|x86.ActiveCfg = Debug|x86 - {1ED83084-2A57-4F89-915C-8A2167C0D6BC}.Debug|x86.Build.0 = Debug|x86 - {1ED83084-2A57-4F89-915C-8A2167C0D6BC}.Publish|Any CPU.ActiveCfg = Publish|x86 - {1ED83084-2A57-4F89-915C-8A2167C0D6BC}.Publish|Any CPU.Build.0 = Publish|x86 - {1ED83084-2A57-4F89-915C-8A2167C0D6BC}.Publish|x86.ActiveCfg = Publish|x86 - {1ED83084-2A57-4F89-915C-8A2167C0D6BC}.Publish|x86.Build.0 = Publish|x86 - {1ED83084-2A57-4F89-915C-8A2167C0D6BC}.Release|Any CPU.ActiveCfg = Release|x86 - {1ED83084-2A57-4F89-915C-8A2167C0D6BC}.Release|x86.ActiveCfg = Release|x86 - {1ED83084-2A57-4F89-915C-8A2167C0D6BC}.Release|x86.Build.0 = Release|x86 - {1ED83084-2A57-4F89-915C-8A2167C0D6BC}.XamlDesign|Any CPU.ActiveCfg = Debug|x86 - {1ED83084-2A57-4F89-915C-8A2167C0D6BC}.XamlDesign|x86.ActiveCfg = Debug|x86 - {1ED83084-2A57-4F89-915C-8A2167C0D6BC}.XamlDesign|x86.Build.0 = Debug|x86 {08DD4305-7787-4823-A53F-4D0F725A07F3}.Debug|Any CPU.ActiveCfg = Release|Any CPU {08DD4305-7787-4823-A53F-4D0F725A07F3}.Debug|Any CPU.Build.0 = Release|Any CPU {08DD4305-7787-4823-A53F-4D0F725A07F3}.Debug|x86.ActiveCfg = Release|Any CPU @@ -374,21 +363,6 @@ Global {600998C4-54DD-4755-BFA8-6F44544D8E2E}.XamlDesign|Any CPU.Build.0 = Release|Any CPU {600998C4-54DD-4755-BFA8-6F44544D8E2E}.XamlDesign|x86.ActiveCfg = Release|Any CPU {600998C4-54DD-4755-BFA8-6F44544D8E2E}.XamlDesign|x86.Build.0 = Release|Any CPU - {3D4AE5F9-A535-4D5C-8F30-1A35D7BA0A3D}.Debug|Any CPU.ActiveCfg = Release|Any CPU - {3D4AE5F9-A535-4D5C-8F30-1A35D7BA0A3D}.Debug|Any CPU.Build.0 = Release|Any CPU - {3D4AE5F9-A535-4D5C-8F30-1A35D7BA0A3D}.Debug|x86.ActiveCfg = Release|Any CPU - {3D4AE5F9-A535-4D5C-8F30-1A35D7BA0A3D}.Debug|x86.Build.0 = Release|Any CPU - {3D4AE5F9-A535-4D5C-8F30-1A35D7BA0A3D}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {3D4AE5F9-A535-4D5C-8F30-1A35D7BA0A3D}.Publish|x86.ActiveCfg = Release|Any CPU - {3D4AE5F9-A535-4D5C-8F30-1A35D7BA0A3D}.Publish|x86.Build.0 = Release|Any CPU - {3D4AE5F9-A535-4D5C-8F30-1A35D7BA0A3D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3D4AE5F9-A535-4D5C-8F30-1A35D7BA0A3D}.Release|Any CPU.Build.0 = Release|Any CPU - {3D4AE5F9-A535-4D5C-8F30-1A35D7BA0A3D}.Release|x86.ActiveCfg = Release|Any CPU - {3D4AE5F9-A535-4D5C-8F30-1A35D7BA0A3D}.Release|x86.Build.0 = Release|Any CPU - {3D4AE5F9-A535-4D5C-8F30-1A35D7BA0A3D}.XamlDesign|Any CPU.ActiveCfg = Release|Any CPU - {3D4AE5F9-A535-4D5C-8F30-1A35D7BA0A3D}.XamlDesign|Any CPU.Build.0 = Release|Any CPU - {3D4AE5F9-A535-4D5C-8F30-1A35D7BA0A3D}.XamlDesign|x86.ActiveCfg = Release|Any CPU - {3D4AE5F9-A535-4D5C-8F30-1A35D7BA0A3D}.XamlDesign|x86.Build.0 = Release|Any CPU {B4E665E5-6CAF-4414-A6E2-8DE1C3BCF203}.Debug|Any CPU.ActiveCfg = Release|Any CPU {B4E665E5-6CAF-4414-A6E2-8DE1C3BCF203}.Debug|Any CPU.Build.0 = Release|Any CPU {B4E665E5-6CAF-4414-A6E2-8DE1C3BCF203}.Debug|x86.ActiveCfg = Release|Any CPU @@ -559,6 +533,22 @@ Global {D1DFBB0C-B570-4302-8F1E-2E3A19C41961}.XamlDesign|Any CPU.Build.0 = Debug|Any CPU {D1DFBB0C-B570-4302-8F1E-2E3A19C41961}.XamlDesign|x86.ActiveCfg = Release|Any CPU {D1DFBB0C-B570-4302-8F1E-2E3A19C41961}.XamlDesign|x86.Build.0 = Release|Any CPU + {1BC94B6A-B021-4207-A70E-936EE272AD3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1BC94B6A-B021-4207-A70E-936EE272AD3E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1BC94B6A-B021-4207-A70E-936EE272AD3E}.Debug|x86.ActiveCfg = Debug|Any CPU + {1BC94B6A-B021-4207-A70E-936EE272AD3E}.Debug|x86.Build.0 = Debug|Any CPU + {1BC94B6A-B021-4207-A70E-936EE272AD3E}.Publish|Any CPU.ActiveCfg = Release|Any CPU + {1BC94B6A-B021-4207-A70E-936EE272AD3E}.Publish|Any CPU.Build.0 = Release|Any CPU + {1BC94B6A-B021-4207-A70E-936EE272AD3E}.Publish|x86.ActiveCfg = Release|Any CPU + {1BC94B6A-B021-4207-A70E-936EE272AD3E}.Publish|x86.Build.0 = Release|Any CPU + {1BC94B6A-B021-4207-A70E-936EE272AD3E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1BC94B6A-B021-4207-A70E-936EE272AD3E}.Release|Any CPU.Build.0 = Release|Any CPU + {1BC94B6A-B021-4207-A70E-936EE272AD3E}.Release|x86.ActiveCfg = Release|Any CPU + {1BC94B6A-B021-4207-A70E-936EE272AD3E}.Release|x86.Build.0 = Release|Any CPU + {1BC94B6A-B021-4207-A70E-936EE272AD3E}.XamlDesign|Any CPU.ActiveCfg = Release|Any CPU + {1BC94B6A-B021-4207-A70E-936EE272AD3E}.XamlDesign|Any CPU.Build.0 = Release|Any CPU + {1BC94B6A-B021-4207-A70E-936EE272AD3E}.XamlDesign|x86.ActiveCfg = Release|Any CPU + {1BC94B6A-B021-4207-A70E-936EE272AD3E}.XamlDesign|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -567,14 +557,12 @@ Global {596595A6-2A3C-469E-9386-9E3767D863A5} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD} {4A84E568-CA86-4510-8CD0-90D3EF9B65F9} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AB8} {8E1F1B4E-AEA2-4AB1-8F73-423A903550A1} = {7B6C5F8D-14B3-443D-B044-0E209AE12BDF} - {1ED83084-2A57-4F89-915C-8A2167C0D6BC} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFF} {1E7F7253-A6AF-43C4-A955-37BEDDA01AC0} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AB8} {08DD4305-7787-4823-A53F-4D0F725A07F3} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AC0} {674B69B8-0780-4D54-AE2B-C15821FA51CB} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AC0} {1E7F7253-A6AF-43C4-A955-37BEDDA01AB9} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AB8} {1CE2D235-8072-4649-BA5A-CFB1AF8776E0} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AB9} {600998C4-54DD-4755-BFA8-6F44544D8E2E} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AB9} - {3D4AE5F9-A535-4D5C-8F30-1A35D7BA0A3D} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AB9} {1E7F7253-A6AF-43C4-A955-37BEDDA01AC9} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AB8} {B4E665E5-6CAF-4414-A6E2-8DE1C3BCF203} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AC9} {241C47DF-CA8E-4296-AA03-2C48BB646ABD} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AC9} @@ -584,5 +572,6 @@ Global {0EC8DBA1-D745-4EE5-993A-6026440EC3BF} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AF9} {DD99FD0F-82F6-4C30-930E-4A1D0DF01D65} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AB9} {7B835A7D-CF94-45E8-B191-96F5A4FE26A8} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD} + {1BC94B6A-B021-4207-A70E-936EE272AD3E} = {7F25BDD5-474F-4EC1-A624-ED946B91F34E} EndGlobalSection EndGlobal diff --git a/appveyor.yml b/appveyor.yml index c83bf0baa5..e59361e408 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -12,7 +12,6 @@ install: Set-Content c:\users\appveyor\.ssh\id_rsa $fileContent } else { git submodule deinit script - git submodule deinit submodules/externalpackages/StartPage } git submodule update diff --git a/script b/script index 15e8e40320..4b7c36795e 160000 --- a/script +++ b/script @@ -1 +1 @@ -Subproject commit 15e8e40320a42797b8a57cedb0cf6f3a146afe1c +Subproject commit 4b7c36795e46704897fd9bee68afd6003dc83b1c diff --git a/src/CredentialManagement/CredentialManagement.csproj b/src/CredentialManagement/CredentialManagement.csproj index d801e2176e..d54af939cb 100644 --- a/src/CredentialManagement/CredentialManagement.csproj +++ b/src/CredentialManagement/CredentialManagement.csproj @@ -38,9 +38,9 @@ false - - ..\..\packages\NullGuard.Fody.1.4.1\Lib\portable-net4+sl4+wp7+win8+MonoAndroid16+MonoTouch40\NullGuard.dll - True + + ..\..\packages\NullGuard.Fody.1.4.6\Lib\dotnet\NullGuard.dll + False @@ -79,12 +79,12 @@ - + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + + + - + + diff --git a/src/GitHub.VisualStudio/GitHubPackage.cs b/src/GitHub.VisualStudio/GitHubPackage.cs index b68d9ba096..8533c53ea3 100644 --- a/src/GitHub.VisualStudio/GitHubPackage.cs +++ b/src/GitHub.VisualStudio/GitHubPackage.cs @@ -10,25 +10,26 @@ using Microsoft.VisualStudio.Shell.Interop; using Octokit; using GitHub.Helpers; -using System.ComponentModel.Design; using System.Diagnostics; using System.Threading; -using tasks = System.Threading.Tasks; -using Microsoft.VisualStudio.ComponentModelHost; +using System.Threading.Tasks; +using Task = System.Threading.Tasks.Task; +using GitHub.VisualStudio.Menus; namespace GitHub.VisualStudio { - [PackageRegistration(UseManagedResourcesOnly = true)] + [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] [InstalledProductRegistration("#110", "#112", System.AssemblyVersionInformation.Version, IconResourceID = 400)] [Guid(GuidList.guidGitHubPkgString)] [ProvideMenuResource("Menus.ctmenu", 1)] // this is the Git service GUID, so we load whenever it loads - [ProvideAutoLoad("11B8E6D7-C08B-4385-B321-321078CDD1F8")] + [ProvideAutoLoad(Guids.GitSccProviderId)] [ProvideToolWindow(typeof(GitHubPane), Orientation = ToolWindowOrientation.Right, Style = VsDockStyle.Tabbed, Window = EnvDTE.Constants.vsWindowKindSolutionExplorer)] [ProvideOptionPage(typeof(OptionsPage), "GitHub for Visual Studio", "General", 0, 0, supportsAutomation: true)] - public class GitHubPackage : Package + public class GitHubPackage : AsyncPackage { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")] readonly IServiceProvider serviceProvider; static GitHubPackage() @@ -46,12 +47,23 @@ public GitHubPackage(IServiceProvider serviceProvider) this.serviceProvider = serviceProvider; } - protected override void Initialize() + protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) { - base.Initialize(); - IncrementLaunchCount(); + await base.InitializeAsync(cancellationToken, progress); + await EnsurePackageLoaded(new Guid(ServiceProviderPackage.ServiceProviderPackageId)); + + // Activate the usage tracker by forcing an instance to be created. + GetServiceAsync(typeof(IUsageTracker)).Forget(); + + InitializeMenus().Forget(); + } + + async Task InitializeMenus() + { + var menus = await GetServiceAsync(typeof(IMenuProvider)) as IMenuProvider; + + await ThreadingHelper.SwitchToMainThreadAsync(); - var menus = serviceProvider.GetExportedValue(); foreach (var menu in menus.Menus) serviceProvider.AddCommandHandler(menu.Guid, menu.CmdId, (s, e) => menu.Activate()); @@ -59,11 +71,16 @@ protected override void Initialize() serviceProvider.AddCommandHandler(menu.Guid, menu.CmdId, menu.CanShow, () => menu.Activate()); } - void IncrementLaunchCount() + async Task EnsurePackageLoaded(Guid packageGuid) { - var usageTracker = serviceProvider.GetExportedValue(); - usageTracker.IncrementLaunchCount(); + var shell = await GetServiceAsync(typeof(SVsShell)) as IVsShell; + if (shell != null) + { + IVsPackage vsPackage; + ErrorHandler.ThrowOnFailure(shell.LoadPackage(ref packageGuid, out vsPackage)); + } } + } [Export(typeof(IGitHubClient))] @@ -78,14 +95,17 @@ public GHClient(IProgram program) [NullGuard.NullGuard(NullGuard.ValidationFlags.None)] [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] + [ProvideService(typeof(IMenuProvider), IsAsyncQueryable = true)] [ProvideService(typeof(IUIProvider), IsAsyncQueryable = true)] + [ProvideService(typeof(IUsageTracker), IsAsyncQueryable = true)] [ProvideAutoLoad(UIContextGuids.NoSolution)] [ProvideAutoLoad(UIContextGuids.SolutionExists)] [Guid(ServiceProviderPackageId)] public sealed class ServiceProviderPackage : AsyncPackage { - const string ServiceProviderPackageId = "D5CE1488-DEDE-426D-9E5B-BFCCFBE33E53"; + public const string ServiceProviderPackageId = "D5CE1488-DEDE-426D-9E5B-BFCCFBE33E53"; const string StartPagePreview4PackageId = "3b764d23-faf7-486f-94c7-b3accc44a70d"; + const string StartPagePreview5PackageId = "3b764d23-faf7-486f-94c7-b3accc44a70e"; Version vsversion; Version VSVersion @@ -112,31 +132,41 @@ Version VSVersion } } - protected override async tasks.Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) + protected override Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) { AddService(typeof(IUIProvider), CreateService, true); - - // Load the start page package only for Dev15 Preview 4 - if (VSVersion.Major == 15 && VSVersion.Build == 25618) - { - var shell = await GetServiceAsync(typeof(SVsShell)) as IVsShell; - IVsPackage startPagePackage; - if (ErrorHandler.Failed(shell?.LoadPackage(new Guid(StartPagePreview4PackageId), out startPagePackage) ?? -1)) - { - // ¯\_(ツ)_/¯ - } - } + AddService(typeof(IUsageTracker), CreateService, true); + AddService(typeof(IMenuProvider), CreateService, true); + return Task.CompletedTask; } - async tasks.Task CreateService(IAsyncServiceContainer container, CancellationToken cancellationToken, Type serviceType) + async Task CreateService(IAsyncServiceContainer container, CancellationToken cancellationToken, Type serviceType) { if (serviceType == null) return null; - string contract = AttributedModelServices.GetContractName(serviceType); - var cm = await GetServiceAsync(typeof(SComponentModel)) as IComponentModel; - if (cm == null) - return null; - return await tasks.Task.Run(() => cm.DefaultExportProvider.GetExportedValueOrDefault(contract)); + + if (serviceType == typeof(IUIProvider)) + { + var result = new GitHubServiceProvider(this); + await result.Initialize(); + return result; + } + else if (serviceType == typeof(IMenuProvider)) + { + var sp = await GetServiceAsync(typeof(IUIProvider)) as IUIProvider; + return new MenuProvider(sp); + } + else if (serviceType == typeof(IUsageTracker)) + { + var uiProvider = await GetServiceAsync(typeof(IUIProvider)) as IUIProvider; + return new UsageTracker(uiProvider); + } + // go the mef route + else + { + var sp = await GetServiceAsync(typeof(IUIProvider)) as IUIProvider; + return sp.TryGetService(serviceType); + } } } } diff --git a/src/GitHub.VisualStudio/Menus/AddConnection.cs b/src/GitHub.VisualStudio/Menus/AddConnection.cs index 2c78c91dce..ffc12353d1 100644 --- a/src/GitHub.VisualStudio/Menus/AddConnection.cs +++ b/src/GitHub.VisualStudio/Menus/AddConnection.cs @@ -1,21 +1,14 @@ -using Microsoft.VisualStudio.Shell; -using System; -using System.ComponentModel.Composition; -using GitHub.Services; +using GitHub.Services; using GitHub.UI; -using GitHub.Extensions; using NullGuard; -using GitHub.Api; +using System; namespace GitHub.VisualStudio.Menus { - [Export(typeof(IMenuHandler))] - [PartCreationPolicy(CreationPolicy.Shared)] public class AddConnection: MenuBase, IMenuHandler { - [ImportingConstructor] - public AddConnection([Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider, ISimpleApiClientFactory apiFactory) - : base(serviceProvider, apiFactory) + public AddConnection(IUIProvider serviceProvider) + : base(serviceProvider) { } diff --git a/src/GitHub.VisualStudio/Menus/CopyLink.cs b/src/GitHub.VisualStudio/Menus/CopyLink.cs index 432ee8671b..ebc502dfc9 100644 --- a/src/GitHub.VisualStudio/Menus/CopyLink.cs +++ b/src/GitHub.VisualStudio/Menus/CopyLink.cs @@ -1,27 +1,17 @@ -using Microsoft.VisualStudio.Shell; -using System; -using System.ComponentModel.Composition; -using System.Windows; -using GitHub.Extensions; -using GitHub.Services; +using GitHub.Services; using GitHub.VisualStudio.UI; using NullGuard; -using GitHub.Api; +using System; +using System.Windows; namespace GitHub.VisualStudio.Menus { - [Export(typeof(IDynamicMenuHandler))] - [PartCreationPolicy(CreationPolicy.Shared)] public class CopyLink : LinkMenuBase, IDynamicMenuHandler { - readonly IUsageTracker usageTracker; - [ImportingConstructor] - public CopyLink([Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider, - IUsageTracker usageTracker, ISimpleApiClientFactory apiFactory) - : base(serviceProvider, apiFactory) + public CopyLink(IUIProvider serviceProvider) + : base(serviceProvider) { - this.usageTracker = usageTracker; } public Guid Guid => GuidList.guidContextMenuSet; @@ -39,13 +29,13 @@ public async void Activate([AllowNull]object data = null) try { Clipboard.SetText(link); - var ns = ServiceProvider.GetExportedValue(); + var ns = ServiceProvider.TryGetService(); ns?.ShowMessage(Resources.LinkCopiedToClipboardMessage); - this.usageTracker.IncrementLinkToGitHubCount(); + await UsageTracker.IncrementLinkToGitHubCount(); } catch { - var ns = ServiceProvider.GetExportedValue(); + var ns = ServiceProvider.TryGetService(); ns?.ShowMessage(Resources.Error_FailedToCopyToClipboard); } } diff --git a/src/GitHub.VisualStudio/Menus/CreateGist.cs b/src/GitHub.VisualStudio/Menus/CreateGist.cs index fb95413ad9..1a50215344 100644 --- a/src/GitHub.VisualStudio/Menus/CreateGist.cs +++ b/src/GitHub.VisualStudio/Menus/CreateGist.cs @@ -1,26 +1,20 @@ -using Microsoft.VisualStudio.Shell; -using System; -using System.ComponentModel.Composition; -using GitHub.Services; +using GitHub.Services; using GitHub.UI; -using GitHub.Extensions; -using System.Diagnostics; using NullGuard; +using System; +using System.Diagnostics; namespace GitHub.VisualStudio.Menus { - [Export(typeof(IDynamicMenuHandler))] - [PartCreationPolicy(CreationPolicy.Shared)] public class CreateGist : MenuBase, IDynamicMenuHandler { readonly Lazy selectedTextProvider; + ISelectedTextProvider SelectedTextProvider => selectedTextProvider.Value; - [ImportingConstructor] - public CreateGist([Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider, - Lazy selectedTextProvider) + public CreateGist(IUIProvider serviceProvider) : base(serviceProvider) { - this.selectedTextProvider = selectedTextProvider; + selectedTextProvider = new Lazy(() => ServiceProvider.TryGetService()); } public Guid Guid { get { return GuidList.guidContextMenuSet; } } @@ -28,9 +22,8 @@ public CreateGist([Import(typeof(SVsServiceProvider))] IServiceProvider serviceP public bool CanShow() { - var stp = selectedTextProvider.Value; - Debug.Assert(stp != null, "Could not get an instance of ISelectedTextProvider"); - return !String.IsNullOrWhiteSpace(stp?.GetSelectedText()); + Debug.Assert(SelectedTextProvider != null, "Could not get an instance of ISelectedTextProvider"); + return !String.IsNullOrWhiteSpace(SelectedTextProvider?.GetSelectedText()); } public void Activate([AllowNull] object data) diff --git a/src/GitHub.VisualStudio/Menus/LinkMenuBase.cs b/src/GitHub.VisualStudio/Menus/LinkMenuBase.cs index 92b72dce34..ea8ace9e12 100644 --- a/src/GitHub.VisualStudio/Menus/LinkMenuBase.cs +++ b/src/GitHub.VisualStudio/Menus/LinkMenuBase.cs @@ -1,6 +1,5 @@ -using GitHub.Api; -using GitHub.Extensions; -using GitHub.Primitives; +using GitHub.Primitives; +using GitHub.Services; using System; using System.Threading.Tasks; @@ -8,15 +7,20 @@ namespace GitHub.VisualStudio.Menus { public class LinkMenuBase: MenuBase { - public LinkMenuBase(IServiceProvider serviceProvider, ISimpleApiClientFactory apiFactory) - : base(serviceProvider, apiFactory) + readonly Lazy usageTracker; + + protected IUsageTracker UsageTracker => usageTracker.Value; + + public LinkMenuBase(IUIProvider serviceProvider) + : base(serviceProvider) { + usageTracker = new Lazy(() => ServiceProvider.TryGetService()); } protected Task GenerateLink() { var repo = ActiveRepo; - var activeDocument = ServiceProvider.GetExportedValue(); + var activeDocument = ServiceProvider.TryGetService(); if (activeDocument == null) return null; return repo.GenerateUrl(activeDocument.Name, activeDocument.StartLine, activeDocument.EndLine); diff --git a/src/GitHub.VisualStudio/Base/MenuBase.cs b/src/GitHub.VisualStudio/Menus/MenuBase.cs similarity index 66% rename from src/GitHub.VisualStudio/Base/MenuBase.cs rename to src/GitHub.VisualStudio/Menus/MenuBase.cs index e43c1738bb..dcf34c90c4 100644 --- a/src/GitHub.VisualStudio/Base/MenuBase.cs +++ b/src/GitHub.VisualStudio/Menus/MenuBase.cs @@ -1,24 +1,22 @@ -using System; -using System.Diagnostics; -using System.Globalization; -using System.Linq; -using GitHub.Extensions; +using GitHub.Api; using GitHub.Models; using GitHub.Primitives; using GitHub.Services; -using System.Threading.Tasks; -using GitHub.Api; -using NullGuard; using GitHub.UI; +using NullGuard; +using System; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; namespace GitHub.VisualStudio { public abstract class MenuBase { - readonly IServiceProvider serviceProvider; - readonly ISimpleApiClientFactory apiFactory; + readonly IUIProvider serviceProvider; + readonly Lazy apiFactory; - protected IServiceProvider ServiceProvider { get { return serviceProvider; } } + protected IUIProvider ServiceProvider { get { return serviceProvider; } } protected ILocalRepositoryModel ActiveRepo { get; private set; } @@ -32,35 +30,29 @@ protected ISimpleApiClient SimpleApiClient set { if (simpleApiClient != value && value == null) - apiFactory.ClearFromCache(simpleApiClient); + ApiFactory.ClearFromCache(simpleApiClient); simpleApiClient = value; } } - protected ISimpleApiClientFactory ApiFactory => apiFactory; + protected ISimpleApiClientFactory ApiFactory => apiFactory.Value; protected MenuBase() - { - } + {} - protected MenuBase(IServiceProvider serviceProvider, ISimpleApiClientFactory apiFactory) - { - this.serviceProvider = serviceProvider; - this.apiFactory = apiFactory; - } - - protected MenuBase(IServiceProvider serviceProvider) + protected MenuBase(IUIProvider serviceProvider) { this.serviceProvider = serviceProvider; + apiFactory = new Lazy(() => ServiceProvider.TryGetService()); } protected ILocalRepositoryModel GetActiveRepo() { - var activeRepo = ServiceProvider.GetExportedValue()?.ActiveRepo; + var activeRepo = ServiceProvider.TryGetService()?.ActiveRepo; // activeRepo can be null at this point because it is set elsewhere as the result of async operation that may not have completed yet. if (activeRepo == null) { - var path = ServiceProvider.GetExportedValue()?.GetActiveRepoPath() ?? String.Empty; + var path = ServiceProvider.TryGetService()?.GetActiveRepoPath() ?? String.Empty; try { activeRepo = !string.IsNullOrEmpty(path) ? new LocalRepositoryModel(path) : null; @@ -75,28 +67,23 @@ protected ILocalRepositoryModel GetActiveRepo() protected void StartFlow(UIControllerFlow controllerFlow) { - var uiProvider = ServiceProvider.GetExportedValue(); - Debug.Assert(uiProvider != null, "MenuBase:StartFlow:No UIProvider available."); - if (uiProvider == null) - return; - IConnection connection = null; if (controllerFlow != UIControllerFlow.Authentication) { var activeRepo = GetActiveRepo(); - connection = ServiceProvider.GetExportedValue()?.Connections + connection = ServiceProvider.TryGetService()?.Connections .FirstOrDefault(c => activeRepo?.CloneUrl?.RepositoryName != null && c.HostAddress.Equals(HostAddress.Create(activeRepo.CloneUrl))); } - uiProvider.RunUI(controllerFlow, connection); + ServiceProvider.RunUI(controllerFlow, connection); } void RefreshRepo() { - ActiveRepo = ServiceProvider.GetExportedValue().ActiveRepo; + ActiveRepo = ServiceProvider.TryGetService().ActiveRepo; if (ActiveRepo == null) { - var vsGitServices = ServiceProvider.GetExportedValue(); + var vsGitServices = ServiceProvider.TryGetService(); string path = vsGitServices?.GetActiveRepoPath() ?? String.Empty; try { @@ -117,8 +104,7 @@ protected async Task IsGitHubRepo() if (uri == null) return false; - Debug.Assert(apiFactory != null, "apiFactory cannot be null. Did you call the right constructor?"); - SimpleApiClient = apiFactory.Create(uri); + SimpleApiClient = ApiFactory.Create(uri); var isdotcom = HostAddress.IsGitHubDotComUri(uri.ToRepositoryUrl()); if (!isdotcom) diff --git a/src/GitHub.VisualStudio/Menus/MenuProvider.cs b/src/GitHub.VisualStudio/Menus/MenuProvider.cs index 082130dde6..01eb7f7182 100644 --- a/src/GitHub.VisualStudio/Menus/MenuProvider.cs +++ b/src/GitHub.VisualStudio/Menus/MenuProvider.cs @@ -1,23 +1,57 @@ -using System.Collections.Generic; +using GitHub.Services; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel.Composition; using System.Linq; +using System; +using Microsoft.VisualStudio.Shell; namespace GitHub.VisualStudio.Menus { + /// + /// This is a thin MEF wrapper around the MenuProvider + /// which is registered as a global VS service. This class just + /// redirects every request to the actual service, and can be + /// thrown away as soon as the caller is done (no state is kept) + /// [Export(typeof(IMenuProvider))] - [PartCreationPolicy(CreationPolicy.Shared)] - public class MenuProvider : IMenuProvider + [PartCreationPolicy(CreationPolicy.NonShared)] + public class MenuProviderDispatcher : IMenuProvider + { + readonly IMenuProvider theRealProvider; + + [ImportingConstructor] + public MenuProviderDispatcher([Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider) + { + theRealProvider = serviceProvider.GetService(typeof(IMenuProvider)) as IMenuProvider; + } + + public IReadOnlyCollection DynamicMenus => theRealProvider.DynamicMenus; + + public IReadOnlyCollection Menus => theRealProvider.Menus; + } + + internal class MenuProvider : IMenuProvider { public IReadOnlyCollection Menus { get; } public IReadOnlyCollection DynamicMenus { get; } - [ImportingConstructor] - public MenuProvider([ImportMany] IEnumerable menus, [ImportMany] IEnumerable dynamicMenus) + public MenuProvider(IUIProvider serviceProvider) { - Menus = new ReadOnlyCollection(menus.ToList()); - DynamicMenus = new ReadOnlyCollection(dynamicMenus.ToList()); + Menus = new List + { + new AddConnection(serviceProvider), + new OpenPullRequests(), + new ShowGitHubPane() + }; + + DynamicMenus = new List + { + new CopyLink(serviceProvider), + new CreateGist(serviceProvider), + new OpenLink(serviceProvider) + }; } } } diff --git a/src/GitHub.VisualStudio/Menus/OpenLink.cs b/src/GitHub.VisualStudio/Menus/OpenLink.cs index 4df0b271a0..cab89f9893 100644 --- a/src/GitHub.VisualStudio/Menus/OpenLink.cs +++ b/src/GitHub.VisualStudio/Menus/OpenLink.cs @@ -1,25 +1,14 @@ -using GitHub.Extensions; -using GitHub.Services; -using Microsoft.VisualStudio.Shell; -using System; -using System.ComponentModel.Composition; +using GitHub.Services; using NullGuard; -using GitHub.Api; +using System; namespace GitHub.VisualStudio.Menus { - [Export(typeof(IDynamicMenuHandler))] - [PartCreationPolicy(CreationPolicy.Shared)] public class OpenLink: LinkMenuBase, IDynamicMenuHandler { - readonly IUsageTracker usageTracker; - - [ImportingConstructor] - public OpenLink([Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider, - IUsageTracker usageTracker, ISimpleApiClientFactory apiFactory) - : base(serviceProvider, apiFactory) + public OpenLink(IUIProvider serviceProvider) + : base(serviceProvider) { - this.usageTracker = usageTracker; } public Guid Guid => GuidList.guidContextMenuSet; @@ -34,10 +23,10 @@ public async void Activate([AllowNull]object data = null) var link = await GenerateLink(); if (link == null) return; - var browser = ServiceProvider.GetExportedValue(); + var browser = ServiceProvider.TryGetService(); browser?.OpenUrl(link.ToUri()); - usageTracker.IncrementOpenInGitHubCount(); + await UsageTracker.IncrementOpenInGitHubCount(); } public bool CanShow() diff --git a/src/GitHub.VisualStudio/Menus/OpenPullRequests.cs b/src/GitHub.VisualStudio/Menus/OpenPullRequests.cs index 64192ee7dd..3d0dcd4c3a 100644 --- a/src/GitHub.VisualStudio/Menus/OpenPullRequests.cs +++ b/src/GitHub.VisualStudio/Menus/OpenPullRequests.cs @@ -3,13 +3,11 @@ using GitHub.VisualStudio.UI; using NullGuard; using System; -using System.ComponentModel.Composition; namespace GitHub.VisualStudio.Menus { [ExportMenu(MenuType = MenuType.OpenPullRequests)] - [PartCreationPolicy(CreationPolicy.Shared)] - public class OpenPullRequests: MenuBase, IMenuHandler + public class OpenPullRequests : MenuBase, IMenuHandler { public Guid Guid => GuidList.guidGitHubCmdSet; public int CmdId => PkgCmdIDList.openPullRequestsCommand; diff --git a/src/GitHub.VisualStudio/Menus/ShowGitHubPane.cs b/src/GitHub.VisualStudio/Menus/ShowGitHubPane.cs index eb9b70aab6..f7fa9269f6 100644 --- a/src/GitHub.VisualStudio/Menus/ShowGitHubPane.cs +++ b/src/GitHub.VisualStudio/Menus/ShowGitHubPane.cs @@ -1,14 +1,9 @@ -using GitHub.Exports; -using GitHub.UI; -using GitHub.VisualStudio.UI; +using GitHub.VisualStudio.UI; using NullGuard; using System; -using System.ComponentModel.Composition; namespace GitHub.VisualStudio.Menus { - [ExportMenu(MenuType = MenuType.GitHubPane)] - [PartCreationPolicy(CreationPolicy.Shared)] public class ShowGitHubPane: MenuBase, IMenuHandler { public Guid Guid => GuidList.guidGitHubCmdSet; diff --git a/src/GitHub.VisualStudio/Services/UIProvider.cs b/src/GitHub.VisualStudio/Services/UIProvider.cs index a992b4549f..1c6d7c0307 100644 --- a/src/GitHub.VisualStudio/Services/UIProvider.cs +++ b/src/GitHub.VisualStudio/Services/UIProvider.cs @@ -9,7 +9,6 @@ using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Windows.Controls; using GitHub.Infrastructure; using GitHub.Models; using GitHub.Services; @@ -18,12 +17,71 @@ using Microsoft.VisualStudio.Shell; using NLog; using NullGuard; +using Task = System.Threading.Tasks.Task; namespace GitHub.VisualStudio { + /// + /// This is a thin MEF wrapper around the GitHubServiceProvider + /// which is registered as a global VS service. This class just + /// redirects every request to the actual service, and can be + /// thrown away as soon as the caller is done (no state is kept) + /// [Export(typeof(IUIProvider))] - [PartCreationPolicy(CreationPolicy.Shared)] - public class UIProvider : IUIProvider, IDisposable + [PartCreationPolicy(CreationPolicy.NonShared)] + [NullGuard(ValidationFlags.None)] + public class GitHubProviderDispatcher : IUIProvider + { + readonly IUIProvider theRealProvider; + + [ImportingConstructor] + public GitHubProviderDispatcher([Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider) + { + theRealProvider = serviceProvider.GetService(typeof(IUIProvider)) as IUIProvider; + } + + public ExportProvider ExportProvider => theRealProvider.ExportProvider; + + public IServiceProvider GitServiceProvider + { + get + { + return theRealProvider.GitServiceProvider; + } + + set + { + theRealProvider.GitServiceProvider = value; + } + } + + public void AddService(Type t, object owner, object instance) => theRealProvider.AddService(t, owner, instance); + + public void AddService(object owner, T instance) => theRealProvider.AddService(owner, instance); + + public object GetService(Type serviceType) => theRealProvider.GetService(serviceType); + + public IObservable ListenToCompletionState() => theRealProvider.ListenToCompletionState(); + + public void RemoveService(Type t, object owner) => theRealProvider.RemoveService(t, owner); + + public void RunUI() => theRealProvider.RunUI(); + + public void RunUI(UIControllerFlow controllerFlow, IConnection connection) => theRealProvider.RunUI(controllerFlow, connection); + + public IObservable SetupUI(UIControllerFlow controllerFlow, IConnection connection) => theRealProvider.SetupUI(controllerFlow, connection); + public object TryGetService(string typename) => theRealProvider.TryGetService(typename); + + public object TryGetService(Type t) => theRealProvider.TryGetService(t); + + public T TryGetService() where T : class => theRealProvider.TryGetService(); + } + + /// + /// This is a globally registered service (see `GitHubPackage`). + /// If you need to access this service via MEF, use the `IUIProvider` type + /// + public class GitHubServiceProvider : IUIProvider, IDisposable { class OwnedComposablePart { @@ -34,54 +92,71 @@ class OwnedComposablePart static readonly Logger log = LogManager.GetCurrentClassLogger(); CompositeDisposable disposables = new CompositeDisposable(); readonly IServiceProvider serviceProvider; - CompositionContainer tempContainer; readonly Dictionary tempParts; ExportLifetimeContext currentUIFlow; readonly Version currentVersion; bool initializingLogging = false; - [AllowNull] - public ExportProvider ExportProvider { get; } + public ExportProvider ExportProvider { get; private set; } + + CompositionContainer tempContainer; + CompositionContainer TempContainer + { + get + { + if (tempContainer == null) + { + tempContainer = AddToDisposables(new CompositionContainer(new ComposablePartExportProvider() + { + SourceProvider = ExportProvider + })); + } + return tempContainer; + } + } [AllowNull] public IServiceProvider GitServiceProvider { get; set; } - bool Initialized { get { return ExportProvider != null; } } - - [ImportingConstructor] - public UIProvider([Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider) + public GitHubServiceProvider(IServiceProvider serviceProvider) { - this.currentVersion = typeof(UIProvider).Assembly.GetName().Version; + this.currentVersion = this.GetType().Assembly.GetName().Version; this.serviceProvider = serviceProvider; - var componentModel = serviceProvider.GetService(typeof(SComponentModel)) as IComponentModel; + tempParts = new Dictionary(); + } + + public async Task Initialize() + { + var asyncProvider = serviceProvider as IAsyncServiceProvider; + IComponentModel componentModel = null; + if (asyncProvider != null) + { + componentModel = await asyncProvider.GetServiceAsync(typeof(SComponentModel)) as IComponentModel; + } + + else + { + componentModel = serviceProvider?.GetService(typeof(SComponentModel)) as IComponentModel; + } + Debug.Assert(componentModel != null, "Service of type SComponentModel not found"); if (componentModel == null) { log.Error("Service of type SComponentModel not found"); return; } - ExportProvider = componentModel.DefaultExportProvider; + ExportProvider = componentModel.DefaultExportProvider; if (ExportProvider == null) { log.Error("DefaultExportProvider could not be obtained."); - return; } - - tempContainer = AddToDisposables(new CompositionContainer(new ComposablePartExportProvider() - { - SourceProvider = ExportProvider - })); - tempParts = new Dictionary(); } [return: AllowNull] public object TryGetService(Type serviceType) { - if (!Initialized) - return null; - if (!initializingLogging && log.Factory.Configuration == null) { initializingLogging = true; @@ -92,11 +167,14 @@ public object TryGetService(Type serviceType) } catch { +#if DEBUG + throw; +#endif } } string contract = AttributedModelServices.GetContractName(serviceType); - var instance = AddToDisposables(tempContainer.GetExportedValueOrDefault(contract)); + var instance = AddToDisposables(TempContainer.GetExportedValueOrDefault(contract)); if (instance != null) return instance; @@ -109,12 +187,9 @@ public object TryGetService(Type serviceType) if (instance != null) return instance; - if (GitServiceProvider != null) - { - instance = GitServiceProvider.GetService(serviceType); - if (instance != null) - return instance; - } + instance = GitServiceProvider?.GetService(serviceType); + if (instance != null) + return instance; return null; } @@ -161,12 +236,6 @@ public void AddService(object owner, T instance) public void AddService(Type t, object owner, object instance) { - if (!Initialized) - { - log.Error("ExportProvider is not initialized, cannot add service."); - return; - } - string contract = AttributedModelServices.GetContractName(t); Debug.Assert(!string.IsNullOrEmpty(contract), "Every type must have a contract name"); @@ -177,7 +246,7 @@ public void AddService(Type t, object owner, object instance) var part = batch.AddExportedValue(contract, instance); Debug.Assert(part != null, "Adding an exported value must return a non-null part"); tempParts.Add(contract, new OwnedComposablePart { Owner = owner, Part = part }); - tempContainer.Compose(batch); + TempContainer.Compose(batch); } /// @@ -188,12 +257,6 @@ public void AddService(Type t, object owner, object instance) /// or if it's null, the service will be removed without checking for ownership public void RemoveService(Type t, [AllowNull] object owner) { - if (!Initialized) - { - log.Error("ExportProvider is not initialized, cannot remove service."); - return; - } - string contract = AttributedModelServices.GetContractName(t); Debug.Assert(!string.IsNullOrEmpty(contract), "Every type must have a contract name"); @@ -205,19 +268,13 @@ public void RemoveService(Type t, [AllowNull] object owner) tempParts.Remove(contract); var batch = new CompositionBatch(); batch.RemovePart(part.Part); - tempContainer.Compose(batch); + TempContainer.Compose(batch); } } UI.WindowController windowController; public IObservable SetupUI(UIControllerFlow controllerFlow, [AllowNull] IConnection connection) { - if (!Initialized) - { - log.Error("ExportProvider is not initialized, cannot setup UI."); - return Observable.Empty(); - } - StopUI(); var factory = TryGetService(typeof(IExportFactoryProvider)) as IExportFactoryProvider; @@ -256,12 +313,6 @@ public IObservable ListenToCompletionState() public void RunUI() { - if (!Initialized) - { - log.Error("ExportProvider is not initialized, cannot run UI."); - return; - } - Debug.Assert(windowController != null, "WindowController is null, did you forget to call SetupUI?"); if (windowController == null) { @@ -280,12 +331,6 @@ public void RunUI() public void RunUI(UIControllerFlow controllerFlow, [AllowNull] IConnection connection) { - if (!Initialized) - { - log.Error("ExportProvider is not initialized, cannot run UI for {0}.", controllerFlow); - return; - } - SetupUI(controllerFlow, connection); try { @@ -299,12 +344,6 @@ public void RunUI(UIControllerFlow controllerFlow, [AllowNull] IConnection conne public void StopUI() { - if (!Initialized) - { - log.Error("ExportProvider is not initialized, cannot stop UI."); - return; - } - StopUI(currentUIFlow); currentUIFlow = null; } diff --git a/src/GitHub.Exports/Services/UsageTracker.cs b/src/GitHub.VisualStudio/Services/UsageTracker.cs similarity index 58% rename from src/GitHub.Exports/Services/UsageTracker.cs rename to src/GitHub.VisualStudio/Services/UsageTracker.cs index 1502c1b8e9..19dc1aef81 100644 --- a/src/GitHub.Exports/Services/UsageTracker.cs +++ b/src/GitHub.VisualStudio/Services/UsageTracker.cs @@ -7,26 +7,27 @@ using System.Windows.Threading; using GitHub.Models; using GitHub.Settings; -using Microsoft.VisualStudio.ComponentModelHost; -using Microsoft.VisualStudio.Shell; using Task = System.Threading.Tasks.Task; using GitHub.Extensions; +using System.Threading.Tasks; +using GitHub.Helpers; namespace GitHub.Services { - [Export(typeof(IUsageTracker))] - [PartCreationPolicy(CreationPolicy.Shared)] public class UsageTracker : IUsageTracker { const string StoreFileName = "ghfvs.usage"; static readonly Calendar cal = CultureInfo.InvariantCulture.Calendar; - readonly IMetricsService client; - readonly IConnectionManager connectionManager; - readonly IPackageSettings userSettings; - readonly IVSServices vsservices; + readonly IUIProvider uiProvider; readonly DispatcherTimer timer; - readonly string storePath; + + IMetricsService client; + IConnectionManager connectionManager; + IPackageSettings userSettings; + IVSServices vsservices; + string storePath; + bool firstRun = true; Func fileExists; Func readAllText; @@ -34,13 +35,10 @@ public class UsageTracker : IUsageTracker Action dirCreate; [ImportingConstructor] - public UsageTracker( - IProgram program, - IConnectionManager connectionManager, - IPackageSettings userSettings, - IVSServices vsservices, - [Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider) + public UsageTracker(IUIProvider uiProvider) { + this.uiProvider = uiProvider; + fileExists = (path) => System.IO.File.Exists(path); readAllText = (path, encoding) => { @@ -63,98 +61,105 @@ public UsageTracker( }; dirCreate = (path) => System.IO.Directory.CreateDirectory(path); - this.connectionManager = connectionManager; - this.userSettings = userSettings; - this.vsservices = vsservices; - this.client = serviceProvider.GetExportedValue(); this.timer = new DispatcherTimer( - TimeSpan.FromMinutes(1), + TimeSpan.FromMinutes(3), DispatcherPriority.Background, TimerTick, Dispatcher.CurrentDispatcher); - this.storePath = System.IO.Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - program.ApplicationName, - StoreFileName); - - userSettings.PropertyChanged += (s, e) => - { - if (e.PropertyName == nameof(userSettings.CollectMetrics)) - { - UpdateTimer(false); - } - }; - UpdateTimer(true); + RunTimer(); } - public void IncrementLaunchCount() + public async Task IncrementLaunchCount() { - var usage = LoadUsage(); + var usage = await LoadUsage(); ++usage.Model.NumberOfStartups; ++usage.Model.NumberOfStartupsWeek; ++usage.Model.NumberOfStartupsMonth; SaveUsage(usage); } - public void IncrementCloneCount() + public async Task IncrementCloneCount() { - var usage = LoadUsage(); + var usage = await LoadUsage(); ++usage.Model.NumberOfClones; SaveUsage(usage); } - public void IncrementCreateCount() + public async Task IncrementCreateCount() { - var usage = LoadUsage(); + var usage = await LoadUsage(); ++usage.Model.NumberOfReposCreated; SaveUsage(usage); } - public void IncrementPublishCount() + public async Task IncrementPublishCount() { - var usage = LoadUsage(); + var usage = await LoadUsage(); ++usage.Model.NumberOfReposPublished; SaveUsage(usage); } - public void IncrementOpenInGitHubCount() + public async Task IncrementOpenInGitHubCount() { - var usage = LoadUsage(); + var usage = await LoadUsage(); ++usage.Model.NumberOfOpenInGitHub; SaveUsage(usage); } - public void IncrementLinkToGitHubCount() + public async Task IncrementLinkToGitHubCount() { - var usage = LoadUsage(); + var usage = await LoadUsage(); ++usage.Model.NumberOfLinkToGitHub; SaveUsage(usage); } - public void IncrementCreateGistCount() + public async Task IncrementCreateGistCount() { - var usage = LoadUsage(); + var usage = await LoadUsage(); ++usage.Model.NumberOfGists; SaveUsage(usage); } - public void IncrementUpstreamPullRequestCount() + public async Task IncrementUpstreamPullRequestCount() { - var usage = LoadUsage(); + var usage = await LoadUsage(); ++usage.Model.NumberOfUpstreamPullRequests; SaveUsage(usage); } - public void IncrementLoginCount() + public async Task IncrementLoginCount() { - var usage = LoadUsage(); + var usage = await LoadUsage(); ++usage.Model.NumberOfLogins; SaveUsage(usage); } - UsageStore LoadUsage() + async Task Initialize() { + // The services needed by the usage tracker are loaded when they are first needed to + // improve the startup time of the extension. + if (userSettings == null) + { + await ThreadingHelper.SwitchToMainThreadAsync(); + + client = uiProvider.GetService(); + connectionManager = uiProvider.GetService(); + userSettings = uiProvider.GetService(); + vsservices = uiProvider.GetService(); + + var program = uiProvider.GetService(); + storePath = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + program.ApplicationName, + StoreFileName); + } + } + + async Task LoadUsage() + { + await Initialize(); + var json = fileExists(storePath) ? readAllText(storePath, Encoding.UTF8) : null; UsageStore result = null; try @@ -182,58 +187,57 @@ void SaveUsage(UsageStore store) writeAllText(storePath, json, Encoding.UTF8); } - void UpdateTimer(bool initialCall) + void RunTimer() { - // If the method was called due to userSettings.CollectMetrics changing, then send an - // opt-in/out message. - if (!initialCall && client != null) - { - if (userSettings.CollectMetrics) - client.SendOptIn(); - else - client.SendOptOut(); - } - - // The timer first ticks after 1 minute to allow things to settle down after startup. + // The timer first ticks after 3 minutes to allow things to settle down after startup. // This will be changed to 8 hours after the first tick by the TimerTick method. - timer.Stop(); - timer.Interval = TimeSpan.FromMinutes(1); + timer.Start(); + } - if (userSettings.CollectMetrics && client != null) - timer.Start(); + void TimerTick(object sender, EventArgs e) + { + TimerTick() + .Catch(ex => + { + //log.Warn("Failed submitting usage data", ex); + }) + .Forget(); } - async void TimerTick(object sender, EventArgs e) + async Task TimerTick() { - Debug.Assert(client != null, "TimerTick should not be triggered when there is no IMetricsService"); + await Initialize(); - // Subsequent timer ticks should occur every 8 hours. - timer.Interval = TimeSpan.FromHours(8); + if (firstRun) + { + await IncrementLaunchCount(); + timer.Interval = TimeSpan.FromHours(8); + firstRun = false; + } - try + if (client == null || !userSettings.CollectMetrics) { - // Every time we increment the launch count we increment both daily and weekly - // launch count but we only submit (and clear) the weekly launch count when we've - // transitioned into a new week. We've defined a week by the ISO8601 definition, - // i.e. week starting on Monday and ending on Sunday. - var usage = LoadUsage(); - var lastDate = usage.LastUpdated; - var currentDate = DateTimeOffset.Now; - var includeWeekly = GetIso8601WeekOfYear(lastDate) != GetIso8601WeekOfYear(currentDate); - var includeMonthly = lastDate.Month != currentDate.Month; - - // Only send stats once a day. - if (lastDate.Date != currentDate.Date) - { - await SendUsage(usage.Model, includeWeekly, includeMonthly); - ClearCounters(usage.Model, includeWeekly, includeMonthly); - usage.LastUpdated = DateTimeOffset.Now.UtcDateTime; - SaveUsage(usage); - } + timer.Stop(); + return; } - catch //(Exception ex) + + // Every time we increment the launch count we increment both daily and weekly + // launch count but we only submit (and clear) the weekly launch count when we've + // transitioned into a new week. We've defined a week by the ISO8601 definition, + // i.e. week starting on Monday and ending on Sunday. + var usage = await LoadUsage(); + var lastDate = usage.LastUpdated; + var currentDate = DateTimeOffset.Now; + var includeWeekly = GetIso8601WeekOfYear(lastDate) != GetIso8601WeekOfYear(currentDate); + var includeMonthly = lastDate.Month != currentDate.Month; + + // Only send stats once a day. + if (lastDate.Date != currentDate.Date) { - //log.Warn("Failed submitting usage data", ex); + await SendUsage(usage.Model, includeWeekly, includeMonthly); + ClearCounters(usage.Model, includeWeekly, includeMonthly); + usage.LastUpdated = DateTimeOffset.Now.UtcDateTime; + SaveUsage(usage); } } @@ -258,7 +262,7 @@ async Task SendUsage(UsageModel usage, bool weekly, bool monthly) // http://blogs.msdn.com/b/shawnste/archive/2006/01/24/iso-8601-week-of-year-format-in-microsoft-net.aspx static int GetIso8601WeekOfYear(DateTimeOffset time) { - // Seriously cheat. If its Monday, Tuesday or Wednesday, then it'll + // Seriously cheat. If its Monday, Tuesday or Wednesday, then it'll // be the same week# as whatever Thursday, Friday or Saturday are, // and we always get those right DayOfWeek day = cal.GetDayOfWeek(time.UtcDateTime); diff --git a/src/GitHub.VisualStudio/Services/UsageTrackerDispatcher.cs b/src/GitHub.VisualStudio/Services/UsageTrackerDispatcher.cs new file mode 100644 index 0000000000..854eb74e76 --- /dev/null +++ b/src/GitHub.VisualStudio/Services/UsageTrackerDispatcher.cs @@ -0,0 +1,30 @@ +using Microsoft.VisualStudio.Shell; +using System; +using System.ComponentModel.Composition; +using Task = System.Threading.Tasks.Task; + +namespace GitHub.Services +{ + [Export(typeof(IUsageTracker))] + [PartCreationPolicy(CreationPolicy.NonShared)] + public class UsageTrackerDispatcher : IUsageTracker + { + readonly IUsageTracker inner; + + [ImportingConstructor] + public UsageTrackerDispatcher([Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider) + { + inner = serviceProvider.GetService(typeof(IUsageTracker)) as IUsageTracker; + } + + public Task IncrementCloneCount() => inner.IncrementCloneCount(); + public Task IncrementCreateCount() => inner.IncrementCreateCount(); + public Task IncrementCreateGistCount() => inner.IncrementCreateGistCount(); + public Task IncrementLaunchCount() => inner.IncrementLaunchCount(); + public Task IncrementLinkToGitHubCount() => inner.IncrementLinkToGitHubCount(); + public Task IncrementLoginCount() => inner.IncrementLoginCount(); + public Task IncrementOpenInGitHubCount() => inner.IncrementOpenInGitHubCount(); + public Task IncrementPublishCount() => inner.IncrementPublishCount(); + public Task IncrementUpstreamPullRequestCount() => inner.IncrementUpstreamPullRequestCount(); + } +} diff --git a/src/GitHub.VisualStudio/Settings/generated/PackageSettingsGen.cs b/src/GitHub.VisualStudio/Settings/generated/PackageSettingsGen.cs new file mode 100644 index 0000000000..cb6aedb8f7 --- /dev/null +++ b/src/GitHub.VisualStudio/Settings/generated/PackageSettingsGen.cs @@ -0,0 +1,57 @@ +// This is an automatically generated file, based on settings.json and PackageSettingsGen.tt +/* settings.json content: +{ + "settings": [ + { + "name": "CollectMetrics", + "type": "bool", + "default": 'true' + }, + { + "name": "UIState", + "type": "object", + "typename": "UIState", + "default": 'null' + } + ] +} +*/ + +using GitHub.Settings; +using GitHub.Primitives; +using GitHub.VisualStudio.Helpers; + +namespace GitHub.VisualStudio.Settings { + + public partial class PackageSettings : NotificationAwareObject, IPackageSettings + { + + bool collectMetrics; + public bool CollectMetrics + { + get { return collectMetrics; } + set { collectMetrics = value; this.RaisePropertyChange(); } + } + + UIState uIState; + public UIState UIState + { + get { return uIState; } + set { uIState = value; this.RaisePropertyChange(); } + } + + + void LoadSettings() + { + CollectMetrics = (bool)settingsStore.Read("CollectMetrics", true); + UIState = SimpleJson.DeserializeObject((string)settingsStore.Read("UIState", "{}")); + } + + void SaveSettings() + { + settingsStore.Write("CollectMetrics", CollectMetrics); + settingsStore.Write("UIState", SimpleJson.SerializeObject(UIState)); + } + + } +} \ No newline at end of file diff --git a/src/GitHub.VisualStudio/UI/Views/Controls/RepositoryCloneControl.xaml b/src/GitHub.VisualStudio/UI/Views/Controls/RepositoryCloneControl.xaml index 957cc5e0ae..8646a01a7a 100644 --- a/src/GitHub.VisualStudio/UI/Views/Controls/RepositoryCloneControl.xaml +++ b/src/GitHub.VisualStudio/UI/Views/Controls/RepositoryCloneControl.xaml @@ -243,7 +243,7 @@ diff --git a/src/GitHub.VisualStudio/UI/Views/Controls/RepositoryCloneControl.xaml.cs b/src/GitHub.VisualStudio/UI/Views/Controls/RepositoryCloneControl.xaml.cs index 4dc83e0f06..84681c461a 100644 --- a/src/GitHub.VisualStudio/UI/Views/Controls/RepositoryCloneControl.xaml.cs +++ b/src/GitHub.VisualStudio/UI/Views/Controls/RepositoryCloneControl.xaml.cs @@ -33,25 +33,45 @@ public partial class RepositoryCloneControl : GenericRepositoryCloneControl { readonly Dictionary groups = new Dictionary(); + static readonly DependencyPropertyKey RepositoriesViewPropertyKey = + DependencyProperty.RegisterReadOnly( + nameof(RepositoriesView), + typeof(ICollectionView), + typeof(RepositoryCloneControl), + new PropertyMetadata(null)); + + public static readonly DependencyProperty RepositoriesViewProperty = RepositoriesViewPropertyKey.DependencyProperty; + public RepositoryCloneControl() { InitializeComponent(); this.WhenActivated(d => { - d(this.OneWayBind(ViewModel, vm => vm.Repositories, v => v.repositoryList.ItemsSource, CreateRepositoryListCollectionView)); d(repositoryList.Events().MouseDoubleClick.InvokeCommand(this, x => x.ViewModel.CloneCommand)); d(ViewModel.CloneCommand.Subscribe(_ => NotifyDone())); }); + IsVisibleChanged += (s, e) => { if (IsVisible) this.TryMoveFocus(FocusNavigationDirection.First).Subscribe(); }; + + this.WhenAnyValue(x => x.ViewModel.Repositories, CreateRepositoryListCollectionView).Subscribe(x => RepositoriesView = x); + } + + public ICollectionView RepositoriesView + { + get { return (ICollectionView)GetValue(RepositoriesViewProperty); } + private set { SetValue(RepositoriesViewPropertyKey, value); } } ListCollectionView CreateRepositoryListCollectionView(IEnumerable repositories) { + if (repositories == null) + return null; + var view = new ListCollectionView((IList)repositories); Debug.Assert(view.GroupDescriptions != null, "view.GroupDescriptions is null"); view.GroupDescriptions.Add(new RepositoryGroupDescription(this)); diff --git a/src/GitHub.VisualStudio/UI/Views/Controls/StartPageCloneView.xaml b/src/GitHub.VisualStudio/UI/Views/Controls/StartPageCloneView.xaml new file mode 100644 index 0000000000..2b7ba49cdf --- /dev/null +++ b/src/GitHub.VisualStudio/UI/Views/Controls/StartPageCloneView.xaml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +