diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..bea4e492df --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +# top-most EditorConfig file +root = true + +[*.cs] +indent_style = space +indent_size = 4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index c950f83d8e..9f7ec8f882 100644 --- a/.gitignore +++ b/.gitignore @@ -16,9 +16,11 @@ x86/ bld/ [Bb]in/ [Oo]bj/ +build/ # Roslyn cache directories *.ide/ +.vs/ # MSTest test Results [Tt]est[Rr]esult*/ @@ -233,6 +235,5 @@ $RECYCLE.BIN/ AkavacheSqliteLinkerOverride.cs NuGetBuild WiX.Toolset.DummyFile.txt -nunit-UnitTests.xml -nunit-TrackingCollectionTests.xml +nunit-*.xml GitHubVS.sln.DotSettings diff --git a/.gitmodules b/.gitmodules index 1e655d0621..f37964cd09 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,21 +1,15 @@ -[submodule "submodules/rothko"] - path = submodules/rothko - url = https://github.com/Haacked/Rothko.git [submodule "submodules/reactiveui"] path = submodules/reactiveui - url = https://github.com/shana/ReactiveUI + url = https://github.com/editor-tools/ReactiveUI [submodule "submodules/octokit.net"] path = submodules/octokit.net - url = https://github.com/shana/Octokit.Net + url = https://github.com/editor-tools/Octokit.Net [submodule "submodules/splat"] path = submodules/splat - url = https://github.com/shana/splat.git + url = https://github.com/editor-tools/splat.git [submodule "submodules/akavache"] path = submodules/akavache - url = https://github.com/shana/Akavache -[submodule "submodules/libgit2sharp"] - path = submodules/libgit2sharp - url = https://github.com/shana/libgit2sharp.git + url = https://github.com/editor-tools/Akavache [submodule "script"] path = script - url = git@github.com:github/VisualStudioBuildScripts + url = git@github.com:github/VisualStudioBuildScripts \ No newline at end of file diff --git a/.ncrunch/Akavache.Sqlite3.v3.ncrunchproject b/.ncrunch/Akavache.Sqlite3.v3.ncrunchproject new file mode 100644 index 0000000000..161aafaa0b --- /dev/null +++ b/.ncrunch/Akavache.Sqlite3.v3.ncrunchproject @@ -0,0 +1,6 @@ + + + False + True + + \ No newline at end of file diff --git a/.ncrunch/Akavache_Net45.v3.ncrunchproject b/.ncrunch/Akavache_Net45.v3.ncrunchproject new file mode 100644 index 0000000000..161aafaa0b --- /dev/null +++ b/.ncrunch/Akavache_Net45.v3.ncrunchproject @@ -0,0 +1,6 @@ + + + False + True + + \ No newline at end of file diff --git a/.ncrunch/Akavache_Portable.v3.ncrunchproject b/.ncrunch/Akavache_Portable.v3.ncrunchproject new file mode 100644 index 0000000000..161aafaa0b --- /dev/null +++ b/.ncrunch/Akavache_Portable.v3.ncrunchproject @@ -0,0 +1,6 @@ + + + False + True + + \ No newline at end of file diff --git a/.ncrunch/CredentialManagement.v3.ncrunchproject b/.ncrunch/CredentialManagement.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/CredentialManagement.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/DesignTimeStyleHelper.v3.ncrunchproject b/.ncrunch/DesignTimeStyleHelper.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/DesignTimeStyleHelper.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/GitHub.Api.v3.ncrunchproject b/.ncrunch/GitHub.Api.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/GitHub.Api.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/GitHub.App.v3.ncrunchproject b/.ncrunch/GitHub.App.v3.ncrunchproject new file mode 100644 index 0000000000..586daf0054 --- /dev/null +++ b/.ncrunch/GitHub.App.v3.ncrunchproject @@ -0,0 +1,6 @@ + + + False + True + + \ No newline at end of file diff --git a/.ncrunch/GitHub.Exports.Reactive.v3.ncrunchproject b/.ncrunch/GitHub.Exports.Reactive.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/GitHub.Exports.Reactive.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/GitHub.Exports.v3.ncrunchproject b/.ncrunch/GitHub.Exports.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/GitHub.Exports.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/GitHub.Extensions.Reactive.v3.ncrunchproject b/.ncrunch/GitHub.Extensions.Reactive.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/GitHub.Extensions.Reactive.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/GitHub.Extensions.v3.ncrunchproject b/.ncrunch/GitHub.Extensions.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/GitHub.Extensions.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/GitHub.InlineReviews.UnitTests.v3.ncrunchproject b/.ncrunch/GitHub.InlineReviews.UnitTests.v3.ncrunchproject new file mode 100644 index 0000000000..0cef203ae5 --- /dev/null +++ b/.ncrunch/GitHub.InlineReviews.UnitTests.v3.ncrunchproject @@ -0,0 +1,9 @@ + + + True + + CopyReferencedAssembliesToWorkspaceIsOn + + True + + \ No newline at end of file diff --git a/.ncrunch/GitHub.InlineReviews.v3.ncrunchproject b/.ncrunch/GitHub.InlineReviews.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/GitHub.InlineReviews.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/GitHub.Logging.v3.ncrunchproject b/.ncrunch/GitHub.Logging.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/GitHub.Logging.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/GitHub.StartPage.v3.ncrunchproject b/.ncrunch/GitHub.StartPage.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/GitHub.StartPage.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/GitHub.TeamFoundation.14.v3.ncrunchproject b/.ncrunch/GitHub.TeamFoundation.14.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/GitHub.TeamFoundation.14.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/GitHub.TeamFoundation.15.v3.ncrunchproject b/.ncrunch/GitHub.TeamFoundation.15.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/GitHub.TeamFoundation.15.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/GitHub.UI.Reactive.v3.ncrunchproject b/.ncrunch/GitHub.UI.Reactive.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/GitHub.UI.Reactive.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/GitHub.UI.UnitTests.v3.ncrunchproject b/.ncrunch/GitHub.UI.UnitTests.v3.ncrunchproject new file mode 100644 index 0000000000..0cef203ae5 --- /dev/null +++ b/.ncrunch/GitHub.UI.UnitTests.v3.ncrunchproject @@ -0,0 +1,9 @@ + + + True + + CopyReferencedAssembliesToWorkspaceIsOn + + True + + \ No newline at end of file diff --git a/.ncrunch/GitHub.UI.v3.ncrunchproject b/.ncrunch/GitHub.UI.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/GitHub.UI.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/GitHub.VisualStudio.UI.v3.ncrunchproject b/.ncrunch/GitHub.VisualStudio.UI.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/GitHub.VisualStudio.UI.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/GitHub.VisualStudio.v3.ncrunchproject b/.ncrunch/GitHub.VisualStudio.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/GitHub.VisualStudio.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/GithubVSTestAutomationIDs.v3.ncrunchproject b/.ncrunch/GithubVSTestAutomationIDs.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/GithubVSTestAutomationIDs.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/Markdig.Wpf.v3.ncrunchproject b/.ncrunch/Markdig.Wpf.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/Markdig.Wpf.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/Octokit.Reactive.v3.ncrunchproject b/.ncrunch/Octokit.Reactive.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/Octokit.Reactive.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/Octokit.v3.ncrunchproject b/.ncrunch/Octokit.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/Octokit.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/ReactiveUI.Events_Net45.v3.ncrunchproject b/.ncrunch/ReactiveUI.Events_Net45.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/ReactiveUI.Events_Net45.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/ReactiveUI.Testing_Net45.v3.ncrunchproject b/.ncrunch/ReactiveUI.Testing_Net45.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/ReactiveUI.Testing_Net45.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/ReactiveUI_Net45.v3.ncrunchproject b/.ncrunch/ReactiveUI_Net45.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/ReactiveUI_Net45.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/Rothko.v3.ncrunchproject b/.ncrunch/Rothko.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/Rothko.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/Splat-Net45.v3.ncrunchproject b/.ncrunch/Splat-Net45.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/Splat-Net45.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/Splat-Portable.v3.ncrunchproject b/.ncrunch/Splat-Portable.v3.ncrunchproject new file mode 100644 index 0000000000..6800b4a3fe --- /dev/null +++ b/.ncrunch/Splat-Portable.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/TrackingCollectionTests.v3.ncrunchproject b/.ncrunch/TrackingCollectionTests.v3.ncrunchproject new file mode 100644 index 0000000000..74f25ff082 --- /dev/null +++ b/.ncrunch/TrackingCollectionTests.v3.ncrunchproject @@ -0,0 +1,15 @@ + + + 5000 + + AbnormalReferenceResolution + + + + TrackingTests.OrderByDoesntMatchOriginalOrderTimings + + + True + True + + \ No newline at end of file diff --git a/.ncrunch/UnitTests.v3.ncrunchproject b/.ncrunch/UnitTests.v3.ncrunchproject new file mode 100644 index 0000000000..8400a07ba0 --- /dev/null +++ b/.ncrunch/UnitTests.v3.ncrunchproject @@ -0,0 +1,22 @@ + + + True + 2000 + + AbnormalReferenceResolution + CopyReferencedAssembliesToWorkspaceIsOn + + + + SimpleRepositoryModelTests + + + UIControllerTests+PullRequestsFlow + + + UIProviderTests + + + True + + \ No newline at end of file diff --git a/Build-Solution.cmd b/Build-Solution.cmd deleted file mode 100644 index 822884e5cc..0000000000 --- a/Build-Solution.cmd +++ /dev/null @@ -1 +0,0 @@ -Powershell -ExecutionPolicy Unrestricted %~dp0Build-Solution.ps1 \ No newline at end of file diff --git a/Build-Solution.ps1 b/Build-Solution.ps1 deleted file mode 100644 index 5e78326dce..0000000000 --- a/Build-Solution.ps1 +++ /dev/null @@ -1,147 +0,0 @@ -param( - [ValidateSet('Full', 'Tests', 'Build', 'Clean')] - [string] - $build = "Build" - , - [ValidateSet('Debug', 'Release')] - [string] - $config = "Release" - , - [ValidateSet('Any CPU', 'x86', 'x64')] - [string] - $platform = "Any CPU" - , - [string] - $verbosity = "minimal" -) - -$rootDirectory = Split-Path $MyInvocation.MyCommand.Path -$projFile = join-path $rootDirectory GitHubVS.msbuild -$msbuild = "C:\Program Files (x86)\MSBuild\14.0\Bin\MSBuild.exe" - -function Die([string]$message, [object[]]$output) { - if ($output) { - Write-Output $output - $message += ". See output above." - } - Throw (New-Object -TypeName ScriptException -ArgumentList $message) -} - -function Run-Command([scriptblock]$Command, [switch]$Fatal, [switch]$Quiet) { - $output = "" - if ($Quiet) { - $output = & $Command 2>&1 - } else { - & $Command - } - - if (!$Fatal) { - return - } - - $exitCode = 0 - if (!$? -and $LastExitCode -ne 0) { - $exitCode = $LastExitCode - } elseif (!$?) { - $exitCode = 1 - } else { - return - } - - Die "``$Command`` failed" $output -} - -function Run-XUnit([string]$project, [int]$timeoutDuration, [string]$configuration) { - $dll = "src\$project\bin\$configuration\$project.dll" - - $xunitDirectory = Join-Path $rootDirectory packages\xunit.runner.console.2.1.0\tools - $consoleRunner = Join-Path $xunitDirectory xunit.console.x86.exe - $xml = Join-Path $rootDirectory "nunit-$project.xml" - $outputPath = [System.IO.Path]::GetTempFileName() - - $args = $dll, "-noshadow", "-xml", $xml, "-parallel", "all" - [object[]] $output = "$consoleRunner " + ($args -join " ") - - $process = Start-Process -PassThru -NoNewWindow -RedirectStandardOutput $outputPath $consoleRunner ($args | %{ "`"$_`"" }) - Wait-Process -InputObject $process -Timeout $timeoutDuration -ErrorAction SilentlyContinue - if ($process.HasExited) { - $output += Get-Content $outputPath - $exitCode = $process.ExitCode - } else { - $output += "Tests timed out. Backtrace:" - $output += Get-DotNetStack $process.Id - $exitCode = 9999 - } - Stop-Process -InputObject $process - Remove-Item $outputPath - - $result = New-Object System.Object - $result | Add-Member -Type NoteProperty -Name Output -Value $output - $result | Add-Member -Type NoteProperty -Name ExitCode -Value $exitCode - $result -} - -function Run-NUnit([string]$project, [int]$timeoutDuration, [string]$configuration) { - $dll = "src\$project\bin\$configuration\$project.dll" - - $nunitDirectory = Join-Path $rootDirectory packages\NUnit.Runners.2.6.4\tools - $consoleRunner = Join-Path $nunitDirectory nunit-console-x86.exe - $xml = Join-Path $rootDirectory "nunit-$project.xml" - $outputPath = [System.IO.Path]::GetTempFileName() - - $args = "-noshadow", "-xml:$xml", "-framework:net-4.5", "-exclude:Timings", $dll - [object[]] $output = "$consoleRunner " + ($args -join " ") - - $process = Start-Process -PassThru -NoNewWindow -RedirectStandardOutput $outputPath $consoleRunner ($args | %{ "`"$_`"" }) - Wait-Process -InputObject $process -Timeout $timeoutDuration -ErrorAction SilentlyContinue - if ($process.HasExited) { - $output += Get-Content $outputPath - $exitCode = $process.ExitCode - } else { - $output += "Tests timed out. Backtrace:" - $output += Get-DotNetStack $process.Id - $exitCode = 9999 - } - - Stop-Process -InputObject $process - Remove-Item $outputPath - - $result = New-Object System.Object - $result | Add-Member -Type NoteProperty -Name Output -Value $output - $result | Add-Member -Type NoteProperty -Name ExitCode -Value $exitCode - $result -} - -function Build-Solution([string]$solution) { - Run-Command -Fatal { & $msbuild $solution /t:Build /property:Configuration=$config /verbosity:$verbosity /p:VisualStudioVersion=14.0 /p:DeployExtension=false } -} - -Write-Output "Building GitHub for Visual Studio..." -Write-Output "" - -Build-Solution GitHubVs.sln - -$exitCode = 0 - -Write-Output "Running Unit Tests..." -$result = Run-XUnit UnitTests 180 $config -if ($result.ExitCode -eq 0) { - # Print out the test result summary. - Write-Output $result.Output[-1] -} else { - $exitCode = $result.ExitCode - Write-Output $result.Output -} - -Write-Output "Running TrackingCollection Tests..." -$result = Run-NUnit TrackingCollectionTests 180 $config -if ($result.ExitCode -eq 0) { - # Print out the test result summary. - Write-Output $result.Output[-3] -} else { - $exitCode = $result.ExitCode - Write-Output $result.Output -} -Write-Output "" - -exit $exitCode \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 788b3848ea..15e2f4ad6d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,26 +1,28 @@ -## Contributing +## Contributing to the _GitHub Extension for Visual Studio_ [fork]: https://github.com/github/VisualStudio/fork [pr]: https://github.com/github/VisualStudio/compare [code-of-conduct]: http://todogroup.org/opencodeofconduct/#VisualStudio/opensource@github.com [readme]: https://github.com/github/VisualStudio#build -Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. +Hi there! We're thrilled that you'd like to contribute to the __GitHub Extension for Visual Studio__. Your help is essential for keeping it great. + +Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE.md). This project adheres to the [Open Code of Conduct][code-of-conduct]. By participating, you are expected to uphold this code. ## Submitting a pull request -0. [Fork][] and clone the repository (see Build Instructions in the [README][readme]) -0. Create a new branch: `git checkout -b my-branch-name` -0. Make your change, add tests, and make sure the tests still pass -0. Push to your fork and [submit a pull request][pr] -0. Pat your self on the back and wait for your pull request to be reviewed and merged. +1. [Fork][] and clone the repository (see Build Instructions in the [README][readme]) +2. Create a new branch: `git checkout -b my-branch-name` +3. Make your change, add tests, and make sure the tests still pass +4. Push to your fork and [submit a pull request][pr] +5. Pat your self on the back and wait for your pull request to be reviewed and merged. Here are a few things you can do that will increase the likelihood of your pull request being accepted: -- Follow the existing code's style. -- Write tests. +- Follow the style/format of the existing code. +- Write tests for your changes. - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). @@ -31,9 +33,33 @@ There are certain areas of the extension that are restricted in what they can do - Team Explorer content outside the Home page is slightly less restricted, but not by much - Dialogs and views that don't inherit from TeamExplorer classes are free to use what they need. +## Submitting an Issue + +### Bug Reporting + +Here are a few helpful tips when reporting a bug: +- Verify that the bug resides in the GitHub for Visual Studio extension + - A lot of functionality provided by this extension resides in the Team Explorer pane, alongside other non-GitHub tools to manage and collaborate on source code, including Visual Studio's Git support, which is owned by Microsoft. + - If this bug not is related to the GitHub extension, visit the [Visual Studio support page](https://www.visualstudio.com/support/support-overview-vs) for help +- Screenshots are very helpful in diagnosing bugs and understanding the state of the extension when it's experiencing problems. Please include them whenever possible. +- A log file is helpful in diagnosing bug issues. To include log files in your issue: + + 1. Close Visual Studio if it's open + 2. Open a Developer Command Prompt for VS2015 + 3. Run devenv /log + 4. Reproduce your issue + 5. Close VS + 6. Locate the following files on your system and email them to windows@github.com or create a gist and link it in the issue report: + - `%appdata%\Microsoft\VisualStudio\14.0\ActivityLog.xml` + - `%localappdata%\temp\extension.log` + - `%localappdata%\GitHubVisualStudio\extension.log` + +### Feature Requests +If you have a feature that you think would be a great addition to the extension, we might already have thought about it too, so be sure to check if your suggestion matches our [roadmap](#roadmap-and-future-feature-ideas) before making a request. Also take a peek at our [pull requests](https://github.com/github/VisualStudio/pulls) to see what we're currently working on. Additionally, someone might have already thought of your idea, so check out Issues labeled as [features](https://github.com/github/VisualStudio/issues?q=is%3Aopen+is%3Aissue+label%3Afeature) to see if it already exists. + ## Things to improve in the current version -- Localization +- [Localization](https://github.com/github/VisualStudio/issues/18) ## Roadmap and future feature ideas diff --git a/GitHubVS.sln b/GitHubVS.sln index cc8c0cb875..97eeb887a0 100644 --- a/GitHubVS.sln +++ b/GitHubVS.sln @@ -1,17 +1,18 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 +# Visual Studio 15 +VisualStudioVersion = 15.0.27703.2035 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 + {7F5ED78B-74A3-4406-A299-70CFB5885B8B} = {7F5ED78B-74A3-4406-A299-70CFB5885B8B} + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Meta", "Meta", "{72036B62-2FA6-4A22-8B33-69F698A18CF1}" ProjectSection(SolutionItems) = preProject README.md = README.md EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "src\UnitTests\UnitTests.csproj", "{596595A6-2A3C-469E-9386-9E3767D863A5}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.UI", "src\GitHub.UI\GitHub.UI.csproj", "{346384DD-2445-4A28-AF22-B45F3957BD89}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.UI.Reactive", "src\GitHub.UI.Reactive\GitHub.UI.Reactive.csproj", "{158B05E8-FDBC-4D71-B871-C96E28D5ADF5}" @@ -23,53 +24,41 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.App", "src\GitHub.App\GitHub.App.csproj", "{1A1DA411-8D1F-4578-80A6-04576BEA2DC5}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Submodules", "Submodules", "{1E7F7253-A6AF-43C4-A955-37BEDDA01AB8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rothko", "submodules\Rothko\src\Rothko.csproj", "{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LibGit2Sharp", "LibGit2Sharp", "{8446C785-A5B4-4676-9B38-560FCA0563E0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibGit2Sharp", "submodules\libgit2sharp\LibGit2Sharp\LibGit2Sharp.csproj", "{EE6ED99F-CB12-4683-B055-D28FC7357A34}" + ProjectSection(SolutionItems) = preProject + test\UnitTests\Args.cs = test\UnitTests\Args.cs + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{8E1F1B4E-AEA2-4AB1-8F73-423A903550A1}" ProjectSection(SolutionItems) = preProject - 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 + scripts\Modules\BuildUtils.psm1 = scripts\Modules\BuildUtils.psm1 + scripts\Modules\Debugging.psm1 = scripts\Modules\Debugging.psm1 + scripts\Modules\Vsix.psm1 = scripts\Modules\Vsix.psm1 EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Script", "Script", "{7B6C5F8D-14B3-443D-B044-0E209AE12BDF}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{7B6C5F8D-14B3-443D-B044-0E209AE12BDF}" ProjectSection(SolutionItems) = preProject .gitattributes = .gitattributes .gitignore = .gitignore - script\Bootstrap.ps1 = script\Bootstrap.ps1 - build.cmd = build.cmd - script\Bump-Version.ps1 = script\Bump-Version.ps1 - script\cibuild.ps1 = script\cibuild.ps1 - script\common.ps1 = script\common.ps1 - script\Deploy.ps1 = script\Deploy.ps1 - script\Get-CheckedOutBranch.ps1 = script\Get-CheckedOutBranch.ps1 - script\HubotTell-NativeRoom.ps1 = script\HubotTell-NativeRoom.ps1 + scripts\build.ps1 = scripts\build.ps1 + scripts\Bump-Version.ps1 = scripts\Bump-Version.ps1 + scripts\common.ps1 = scripts\common.ps1 + scripts\Get-CheckedOutBranch.ps1 = scripts\Get-CheckedOutBranch.ps1 + scripts\Get-HeadSha1.ps1 = scripts\Get-HeadSha1.ps1 nuget.config = nuget.config - script\Require-CleanWorkTree.ps1 = script\Require-CleanWorkTree.ps1 - script\SolutionInfo.cs = script\SolutionInfo.cs - script\Upload-DirectoryToS3.ps1 = script\Upload-DirectoryToS3.ps1 + scripts\Require-CleanWorkTree.ps1 = scripts\Require-CleanWorkTree.ps1 + scripts\Run-NUnit.ps1 = scripts\Run-NUnit.ps1 + scripts\Run-Tests.ps1 = scripts\Run-Tests.ps1 + scripts\Run-XUnit.ps1 = scripts\Run-XUnit.ps1 EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{8A7DA2E7-262B-4581-807A-1C45CE79CDFD}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Support", "Support", "{8A7DA2E7-262B-4581-807A-1C45CE79CDFF}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Exports", "src\GitHub.Exports\GitHub.Exports.csproj", "{9AEA02DB-02B5-409C-B0CA-115D05331A6B}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Api", "src\GitHub.Api\GitHub.Api.csproj", "{B389ADAF-62CC-486E-85B4-2D8B078DF763}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Exports.Reactive", "src\GitHub.Exports.Reactive\GitHub.Exports.Reactive.csproj", "{E4ED0537-D1D9-44B6-9212-3096D7C3F7A1}" 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}" @@ -82,365 +71,472 @@ 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}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akavache.Sqlite3", "submodules\akavache\Akavache.Sqlite3\Akavache.Sqlite3.csproj", "{241C47DF-CA8E-4296-AA03-2C48BB646ABD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akavache_Portable", "submodules\akavache\Akavache\Akavache_Portable.csproj", "{EB73ADDD-2FE9-44C0-A1AB-20709B979B64}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Splat", "Splat", "{1E7F7253-A6AF-43C4-A955-37BEDDA01AF9}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Splat-Net45", "submodules\splat\Splat\Splat-Net45.csproj", "{252CE1C2-027A-4445-A3C2-E4D6C80A935A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Splat-Portable", "submodules\splat\Splat\Splat-Portable.csproj", "{0EC8DBA1-D745-4EE5-993A-6026440EC3BF}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CredentialManagement", "src\CredentialManagement\CredentialManagement.csproj", "{41A47C5B-C606-45B4-B83C-22B9239E4DA0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveUI.Testing_Net45", "submodules\reactiveui\ReactiveUI.Testing\ReactiveUI.Testing_Net45.csproj", "{DD99FD0F-82F6-4C30-930E-4A1D0DF01D65}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrackingCollectionTests", "test\TrackingCollectionTests\TrackingCollectionTests.csproj", "{7B835A7D-CF94-45E8-B191-96F5A4FE26A8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.TeamFoundation.14", "src\GitHub.TeamFoundation.14\GitHub.TeamFoundation.14.csproj", "{161DBF01-1DBF-4B00-8551-C5C00F26720D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.TeamFoundation.15", "src\GitHub.TeamFoundation.15\GitHub.TeamFoundation.15.csproj", "{161DBF01-1DBF-4B00-8551-C5C00F26720E}" +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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.StartPage", "src\GitHub.StartPage\GitHub.StartPage.csproj", "{50E277B8-8580-487A-8F8E-5C3B9FBF0F77}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrackingCollectionTests", "src\TrackingCollectionTests\TrackingCollectionTests.csproj", "{7B835A7D-CF94-45E8-B191-96F5A4FE26A8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.UI.UnitTests", "test\GitHub.UI.UnitTests\GitHub.UI.UnitTests.csproj", "{110B206F-8554-4B51-BF86-94DAA32F5E26}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.InlineReviews", "src\GitHub.InlineReviews\GitHub.InlineReviews.csproj", "{7F5ED78B-74A3-4406-A299-70CFB5885B8B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.InlineReviews.UnitTests", "test\GitHub.InlineReviews.UnitTests\GitHub.InlineReviews.UnitTests.csproj", "{17EB676B-BB91-48B5-AA59-C67695C647C2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Logging", "src\GitHub.Logging\GitHub.Logging.csproj", "{8D73575A-A89F-47CC-B153-B47DD06837F0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Services.Vssdk", "src\GitHub.Services.Vssdk\GitHub.Services.Vssdk.csproj", "{2D3D2834-33BE-45CA-B3CC-12F853557D7B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Metrics", "Metrics", "{C2D88962-BD6B-4F11-B914-535B38377962}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MetricsServer", "test\MetricsTests\MetricsServer\MetricsServer.csproj", "{14FDEE91-7301-4247-846C-049647BF8E99}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MetricsTests", "test\MetricsTests\MetricsTests\MetricsTests.csproj", "{09313E65-7ADB-48C1-AD3A-572020C5BDCB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Api.UnitTests", "test\GitHub.Api.UnitTests\GitHub.Api.UnitTests.csproj", "{EFDE0798-ACDB-431D-B7F1-548A7231C853}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.App.UnitTests", "test\GitHub.App.UnitTests\GitHub.App.UnitTests.csproj", "{3525D819-6AEC-4879-89FB-56B41F026571}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Exports.UnitTests", "test\GitHub.Exports.UnitTests\GitHub.Exports.UnitTests.csproj", "{94509FCB-6C97-4ED6-AED6-6E74AB3CA336}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Exports.Reactive.UnitTests", "test\GitHub.Exports.Reactive.UnitTests\GitHub.Exports.Reactive.UnitTests.csproj", "{C59868FC-D8BC-4D47-B4F3-16908D2641C6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Extensions.UnitTests", "test\GitHub.Extensions.UnitTests\GitHub.Extensions.UnitTests.csproj", "{DE704BBB-6EC6-4173-B695-D9EBF5AEB092}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Primitives.UnitTests", "test\GitHub.Primitives.UnitTests\GitHub.Primitives.UnitTests.csproj", "{E687457A-BEDC-422D-8D9D-2DA58099EBBA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.TeamFoundation.UnitTests", "test\GitHub.TeamFoundation.UnitTests\GitHub.TeamFoundation.UnitTests.csproj", "{93778A89-3E58-4853-B772-948EBB3F17BE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.VisualStudio.UnitTests", "test\GitHub.VisualStudio.UnitTests\GitHub.VisualStudio.UnitTests.csproj", "{8B14F90B-0781-465D-AB94-19C8C56E3A94}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU - Debug|x86 = Debug|x86 - Publish|Any CPU = Publish|Any CPU - Publish|x86 = Publish|x86 + DebugCodeAnalysis|Any CPU = DebugCodeAnalysis|Any CPU + DebugWithoutVsix|Any CPU = DebugWithoutVsix|Any CPU Release|Any CPU = Release|Any CPU - Release|x86 = Release|x86 + ReleaseWithoutVsix|Any CPU = ReleaseWithoutVsix|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Debug|x86.ActiveCfg = Debug|Any CPU - {11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Debug|x86.Build.0 = Debug|Any CPU - {11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Publish|x86.ActiveCfg = Release|Any CPU - {11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Publish|x86.Build.0 = Release|Any CPU + {11569514-5AE5-4B5B-92A2-F10B0967DE5F}.DebugCodeAnalysis|Any CPU.ActiveCfg = DebugCodeAnalysis|Any CPU + {11569514-5AE5-4B5B-92A2-F10B0967DE5F}.DebugCodeAnalysis|Any CPU.Build.0 = DebugCodeAnalysis|Any CPU + {11569514-5AE5-4B5B-92A2-F10B0967DE5F}.DebugWithoutVsix|Any CPU.ActiveCfg = DebugWithoutVsix|Any CPU + {11569514-5AE5-4B5B-92A2-F10B0967DE5F}.DebugWithoutVsix|Any CPU.Build.0 = DebugWithoutVsix|Any CPU {11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Release|Any CPU.ActiveCfg = Release|Any CPU {11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Release|Any CPU.Build.0 = Release|Any CPU - {11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Release|x86.ActiveCfg = Release|Any CPU - {11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Release|x86.Build.0 = Release|Any CPU - {596595A6-2A3C-469E-9386-9E3767D863A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {596595A6-2A3C-469E-9386-9E3767D863A5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {596595A6-2A3C-469E-9386-9E3767D863A5}.Debug|x86.ActiveCfg = Debug|Any CPU - {596595A6-2A3C-469E-9386-9E3767D863A5}.Debug|x86.Build.0 = Debug|Any CPU - {596595A6-2A3C-469E-9386-9E3767D863A5}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {596595A6-2A3C-469E-9386-9E3767D863A5}.Publish|x86.ActiveCfg = Release|Any CPU - {596595A6-2A3C-469E-9386-9E3767D863A5}.Publish|x86.Build.0 = Release|Any CPU - {596595A6-2A3C-469E-9386-9E3767D863A5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {596595A6-2A3C-469E-9386-9E3767D863A5}.Release|Any CPU.Build.0 = Release|Any CPU - {596595A6-2A3C-469E-9386-9E3767D863A5}.Release|x86.ActiveCfg = Release|Any CPU - {596595A6-2A3C-469E-9386-9E3767D863A5}.Release|x86.Build.0 = Release|Any CPU + {11569514-5AE5-4B5B-92A2-F10B0967DE5F}.ReleaseWithoutVsix|Any CPU.ActiveCfg = ReleaseWithoutVsix|Any CPU + {11569514-5AE5-4B5B-92A2-F10B0967DE5F}.ReleaseWithoutVsix|Any CPU.Build.0 = ReleaseWithoutVsix|Any CPU {346384DD-2445-4A28-AF22-B45F3957BD89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {346384DD-2445-4A28-AF22-B45F3957BD89}.Debug|Any CPU.Build.0 = Debug|Any CPU - {346384DD-2445-4A28-AF22-B45F3957BD89}.Debug|x86.ActiveCfg = Debug|Any CPU - {346384DD-2445-4A28-AF22-B45F3957BD89}.Debug|x86.Build.0 = Debug|Any CPU - {346384DD-2445-4A28-AF22-B45F3957BD89}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {346384DD-2445-4A28-AF22-B45F3957BD89}.Publish|x86.ActiveCfg = Release|Any CPU - {346384DD-2445-4A28-AF22-B45F3957BD89}.Publish|x86.Build.0 = Release|Any CPU + {346384DD-2445-4A28-AF22-B45F3957BD89}.DebugCodeAnalysis|Any CPU.ActiveCfg = DebugCodeAnalysis|Any CPU + {346384DD-2445-4A28-AF22-B45F3957BD89}.DebugCodeAnalysis|Any CPU.Build.0 = DebugCodeAnalysis|Any CPU + {346384DD-2445-4A28-AF22-B45F3957BD89}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {346384DD-2445-4A28-AF22-B45F3957BD89}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU {346384DD-2445-4A28-AF22-B45F3957BD89}.Release|Any CPU.ActiveCfg = Release|Any CPU {346384DD-2445-4A28-AF22-B45F3957BD89}.Release|Any CPU.Build.0 = Release|Any CPU - {346384DD-2445-4A28-AF22-B45F3957BD89}.Release|x86.ActiveCfg = Release|Any CPU - {346384DD-2445-4A28-AF22-B45F3957BD89}.Release|x86.Build.0 = Release|Any CPU + {346384DD-2445-4A28-AF22-B45F3957BD89}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {346384DD-2445-4A28-AF22-B45F3957BD89}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU {158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.Debug|x86.ActiveCfg = Debug|Any CPU - {158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.Debug|x86.Build.0 = Debug|Any CPU - {158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.Publish|x86.ActiveCfg = Release|Any CPU - {158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.Publish|x86.Build.0 = Release|Any CPU + {158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.DebugCodeAnalysis|Any CPU.ActiveCfg = DebugCodeAnalysis|Any CPU + {158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.DebugCodeAnalysis|Any CPU.Build.0 = DebugCodeAnalysis|Any CPU + {158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU {158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.Release|Any CPU.ActiveCfg = Release|Any CPU {158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.Release|Any CPU.Build.0 = Release|Any CPU - {158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.Release|x86.ActiveCfg = Release|Any CPU - {158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.Release|x86.Build.0 = Release|Any CPU + {158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU {6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.Debug|x86.ActiveCfg = Debug|Any CPU - {6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.Debug|x86.Build.0 = Debug|Any CPU - {6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.Publish|x86.ActiveCfg = Release|Any CPU - {6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.Publish|x86.Build.0 = Release|Any CPU + {6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.DebugCodeAnalysis|Any CPU.ActiveCfg = DebugCodeAnalysis|Any CPU + {6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.DebugCodeAnalysis|Any CPU.Build.0 = DebugCodeAnalysis|Any CPU + {6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU {6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.Release|Any CPU.ActiveCfg = Release|Any CPU {6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.Release|Any CPU.Build.0 = Release|Any CPU - {6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.Release|x86.ActiveCfg = Release|Any CPU - {6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.Release|x86.Build.0 = Release|Any CPU + {6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU {6559E128-8B40-49A5-85A8-05565ED0C7E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6559E128-8B40-49A5-85A8-05565ED0C7E3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6559E128-8B40-49A5-85A8-05565ED0C7E3}.Debug|x86.ActiveCfg = Debug|Any CPU - {6559E128-8B40-49A5-85A8-05565ED0C7E3}.Debug|x86.Build.0 = Debug|Any CPU - {6559E128-8B40-49A5-85A8-05565ED0C7E3}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {6559E128-8B40-49A5-85A8-05565ED0C7E3}.Publish|x86.ActiveCfg = Release|Any CPU - {6559E128-8B40-49A5-85A8-05565ED0C7E3}.Publish|x86.Build.0 = Release|Any CPU + {6559E128-8B40-49A5-85A8-05565ED0C7E3}.DebugCodeAnalysis|Any CPU.ActiveCfg = DebugCodeAnalysis|Any CPU + {6559E128-8B40-49A5-85A8-05565ED0C7E3}.DebugCodeAnalysis|Any CPU.Build.0 = DebugCodeAnalysis|Any CPU + {6559E128-8B40-49A5-85A8-05565ED0C7E3}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {6559E128-8B40-49A5-85A8-05565ED0C7E3}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU {6559E128-8B40-49A5-85A8-05565ED0C7E3}.Release|Any CPU.ActiveCfg = Release|Any CPU {6559E128-8B40-49A5-85A8-05565ED0C7E3}.Release|Any CPU.Build.0 = Release|Any CPU - {6559E128-8B40-49A5-85A8-05565ED0C7E3}.Release|x86.ActiveCfg = Release|Any CPU - {6559E128-8B40-49A5-85A8-05565ED0C7E3}.Release|x86.Build.0 = Release|Any CPU + {6559E128-8B40-49A5-85A8-05565ED0C7E3}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {6559E128-8B40-49A5-85A8-05565ED0C7E3}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU {1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.Debug|x86.ActiveCfg = Debug|Any CPU - {1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.Debug|x86.Build.0 = Debug|Any CPU - {1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.Publish|x86.ActiveCfg = Release|Any CPU - {1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.Publish|x86.Build.0 = Release|Any CPU + {1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.DebugCodeAnalysis|Any CPU.ActiveCfg = DebugCodeAnalysis|Any CPU + {1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.DebugCodeAnalysis|Any CPU.Build.0 = DebugCodeAnalysis|Any CPU + {1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU {1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.Release|Any CPU.ActiveCfg = Release|Any CPU {1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.Release|Any CPU.Build.0 = Release|Any CPU - {1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.Release|x86.ActiveCfg = Release|Any CPU - {1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.Release|x86.Build.0 = Release|Any CPU - {4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Debug|Any CPU.ActiveCfg = Release|Any CPU - {4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Debug|Any CPU.Build.0 = Release|Any CPU - {4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Debug|x86.ActiveCfg = Debug|Any CPU - {4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Debug|x86.Build.0 = Debug|Any CPU - {4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Publish|x86.ActiveCfg = Release|Any CPU - {4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Publish|x86.Build.0 = Release|Any CPU - {4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Release|Any CPU.Build.0 = Release|Any CPU - {4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Release|x86.ActiveCfg = Release|Any CPU - {4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Release|x86.Build.0 = Release|Any CPU - {EE6ED99F-CB12-4683-B055-D28FC7357A34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EE6ED99F-CB12-4683-B055-D28FC7357A34}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EE6ED99F-CB12-4683-B055-D28FC7357A34}.Debug|x86.ActiveCfg = Debug|Any CPU - {EE6ED99F-CB12-4683-B055-D28FC7357A34}.Debug|x86.Build.0 = Debug|Any CPU - {EE6ED99F-CB12-4683-B055-D28FC7357A34}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {EE6ED99F-CB12-4683-B055-D28FC7357A34}.Publish|Any CPU.Build.0 = Release|Any CPU - {EE6ED99F-CB12-4683-B055-D28FC7357A34}.Publish|x86.ActiveCfg = Release|Any CPU - {EE6ED99F-CB12-4683-B055-D28FC7357A34}.Publish|x86.Build.0 = Release|Any CPU - {EE6ED99F-CB12-4683-B055-D28FC7357A34}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EE6ED99F-CB12-4683-B055-D28FC7357A34}.Release|Any CPU.Build.0 = Release|Any CPU - {EE6ED99F-CB12-4683-B055-D28FC7357A34}.Release|x86.ActiveCfg = Release|Any CPU - {EE6ED99F-CB12-4683-B055-D28FC7357A34}.Release|x86.Build.0 = Release|Any CPU + {1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU {9AEA02DB-02B5-409C-B0CA-115D05331A6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9AEA02DB-02B5-409C-B0CA-115D05331A6B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9AEA02DB-02B5-409C-B0CA-115D05331A6B}.Debug|x86.ActiveCfg = Debug|Any CPU - {9AEA02DB-02B5-409C-B0CA-115D05331A6B}.Debug|x86.Build.0 = Debug|Any CPU - {9AEA02DB-02B5-409C-B0CA-115D05331A6B}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {9AEA02DB-02B5-409C-B0CA-115D05331A6B}.Publish|x86.ActiveCfg = Release|Any CPU - {9AEA02DB-02B5-409C-B0CA-115D05331A6B}.Publish|x86.Build.0 = Release|Any CPU + {9AEA02DB-02B5-409C-B0CA-115D05331A6B}.DebugCodeAnalysis|Any CPU.ActiveCfg = DebugCodeAnalysis|Any CPU + {9AEA02DB-02B5-409C-B0CA-115D05331A6B}.DebugCodeAnalysis|Any CPU.Build.0 = DebugCodeAnalysis|Any CPU + {9AEA02DB-02B5-409C-B0CA-115D05331A6B}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {9AEA02DB-02B5-409C-B0CA-115D05331A6B}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU {9AEA02DB-02B5-409C-B0CA-115D05331A6B}.Release|Any CPU.ActiveCfg = Release|Any CPU {9AEA02DB-02B5-409C-B0CA-115D05331A6B}.Release|Any CPU.Build.0 = Release|Any CPU - {9AEA02DB-02B5-409C-B0CA-115D05331A6B}.Release|x86.ActiveCfg = Release|Any CPU - {9AEA02DB-02B5-409C-B0CA-115D05331A6B}.Release|x86.Build.0 = Release|Any CPU + {9AEA02DB-02B5-409C-B0CA-115D05331A6B}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {9AEA02DB-02B5-409C-B0CA-115D05331A6B}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Debug|x86.ActiveCfg = Debug|Any CPU - {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Debug|x86.Build.0 = Debug|Any CPU - {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Publish|x86.ActiveCfg = Release|Any CPU - {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Publish|x86.Build.0 = Release|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF763}.DebugCodeAnalysis|Any CPU.ActiveCfg = DebugCodeAnalysis|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF763}.DebugCodeAnalysis|Any CPU.Build.0 = DebugCodeAnalysis|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF763}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF763}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Release|Any CPU.ActiveCfg = Release|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Release|Any CPU.Build.0 = Release|Any CPU - {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Release|x86.ActiveCfg = Release|Any CPU - {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Release|x86.Build.0 = Release|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF763}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF763}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU {E4ED0537-D1D9-44B6-9212-3096D7C3F7A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E4ED0537-D1D9-44B6-9212-3096D7C3F7A1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E4ED0537-D1D9-44B6-9212-3096D7C3F7A1}.Debug|x86.ActiveCfg = Debug|Any CPU - {E4ED0537-D1D9-44B6-9212-3096D7C3F7A1}.Debug|x86.Build.0 = Debug|Any CPU - {E4ED0537-D1D9-44B6-9212-3096D7C3F7A1}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {E4ED0537-D1D9-44B6-9212-3096D7C3F7A1}.Publish|x86.ActiveCfg = Release|Any CPU - {E4ED0537-D1D9-44B6-9212-3096D7C3F7A1}.Publish|x86.Build.0 = Release|Any CPU + {E4ED0537-D1D9-44B6-9212-3096D7C3F7A1}.DebugCodeAnalysis|Any CPU.ActiveCfg = DebugCodeAnalysis|Any CPU + {E4ED0537-D1D9-44B6-9212-3096D7C3F7A1}.DebugCodeAnalysis|Any CPU.Build.0 = DebugCodeAnalysis|Any CPU + {E4ED0537-D1D9-44B6-9212-3096D7C3F7A1}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {E4ED0537-D1D9-44B6-9212-3096D7C3F7A1}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU {E4ED0537-D1D9-44B6-9212-3096D7C3F7A1}.Release|Any CPU.ActiveCfg = Release|Any CPU {E4ED0537-D1D9-44B6-9212-3096D7C3F7A1}.Release|Any CPU.Build.0 = Release|Any CPU - {E4ED0537-D1D9-44B6-9212-3096D7C3F7A1}.Release|x86.ActiveCfg = Release|Any CPU - {E4ED0537-D1D9-44B6-9212-3096D7C3F7A1}.Release|x86.Build.0 = Release|Any CPU - {B1F5C227-456F-437D-BD5F-4C11B7A8D1A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B1F5C227-456F-437D-BD5F-4C11B7A8D1A0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B1F5C227-456F-437D-BD5F-4C11B7A8D1A0}.Debug|x86.ActiveCfg = Debug|Any CPU - {B1F5C227-456F-437D-BD5F-4C11B7A8D1A0}.Debug|x86.Build.0 = Debug|Any CPU - {B1F5C227-456F-437D-BD5F-4C11B7A8D1A0}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {B1F5C227-456F-437D-BD5F-4C11B7A8D1A0}.Publish|x86.ActiveCfg = Release|Any CPU - {B1F5C227-456F-437D-BD5F-4C11B7A8D1A0}.Publish|x86.Build.0 = Release|Any CPU - {B1F5C227-456F-437D-BD5F-4C11B7A8D1A0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B1F5C227-456F-437D-BD5F-4C11B7A8D1A0}.Release|Any CPU.Build.0 = Release|Any CPU - {B1F5C227-456F-437D-BD5F-4C11B7A8D1A0}.Release|x86.ActiveCfg = Release|Any CPU - {B1F5C227-456F-437D-BD5F-4C11B7A8D1A0}.Release|x86.Build.0 = Release|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 - {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 = Debug|Any CPU - {08DD4305-7787-4823-A53F-4D0F725A07F3}.Debug|x86.Build.0 = Debug|Any CPU - {08DD4305-7787-4823-A53F-4D0F725A07F3}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {08DD4305-7787-4823-A53F-4D0F725A07F3}.Publish|x86.ActiveCfg = Release|Any CPU - {08DD4305-7787-4823-A53F-4D0F725A07F3}.Publish|x86.Build.0 = Release|Any CPU + {E4ED0537-D1D9-44B6-9212-3096D7C3F7A1}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {E4ED0537-D1D9-44B6-9212-3096D7C3F7A1}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU + {08DD4305-7787-4823-A53F-4D0F725A07F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08DD4305-7787-4823-A53F-4D0F725A07F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08DD4305-7787-4823-A53F-4D0F725A07F3}.DebugCodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU + {08DD4305-7787-4823-A53F-4D0F725A07F3}.DebugCodeAnalysis|Any CPU.Build.0 = Release|Any CPU + {08DD4305-7787-4823-A53F-4D0F725A07F3}.DebugWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {08DD4305-7787-4823-A53F-4D0F725A07F3}.DebugWithoutVsix|Any CPU.Build.0 = Release|Any CPU {08DD4305-7787-4823-A53F-4D0F725A07F3}.Release|Any CPU.ActiveCfg = Release|Any CPU {08DD4305-7787-4823-A53F-4D0F725A07F3}.Release|Any CPU.Build.0 = Release|Any CPU - {08DD4305-7787-4823-A53F-4D0F725A07F3}.Release|x86.ActiveCfg = Release|Any CPU - {08DD4305-7787-4823-A53F-4D0F725A07F3}.Release|x86.Build.0 = Release|Any CPU + {08DD4305-7787-4823-A53F-4D0F725A07F3}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {08DD4305-7787-4823-A53F-4D0F725A07F3}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU {674B69B8-0780-4D54-AE2B-C15821FA51CB}.Debug|Any CPU.ActiveCfg = Release|Any CPU {674B69B8-0780-4D54-AE2B-C15821FA51CB}.Debug|Any CPU.Build.0 = Release|Any CPU - {674B69B8-0780-4D54-AE2B-C15821FA51CB}.Debug|x86.ActiveCfg = Debug|Any CPU - {674B69B8-0780-4D54-AE2B-C15821FA51CB}.Debug|x86.Build.0 = Debug|Any CPU - {674B69B8-0780-4D54-AE2B-C15821FA51CB}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {674B69B8-0780-4D54-AE2B-C15821FA51CB}.Publish|x86.ActiveCfg = Release|Any CPU - {674B69B8-0780-4D54-AE2B-C15821FA51CB}.Publish|x86.Build.0 = Release|Any CPU + {674B69B8-0780-4D54-AE2B-C15821FA51CB}.DebugCodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU + {674B69B8-0780-4D54-AE2B-C15821FA51CB}.DebugCodeAnalysis|Any CPU.Build.0 = Release|Any CPU + {674B69B8-0780-4D54-AE2B-C15821FA51CB}.DebugWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {674B69B8-0780-4D54-AE2B-C15821FA51CB}.DebugWithoutVsix|Any CPU.Build.0 = Release|Any CPU {674B69B8-0780-4D54-AE2B-C15821FA51CB}.Release|Any CPU.ActiveCfg = Release|Any CPU {674B69B8-0780-4D54-AE2B-C15821FA51CB}.Release|Any CPU.Build.0 = Release|Any CPU - {674B69B8-0780-4D54-AE2B-C15821FA51CB}.Release|x86.ActiveCfg = Release|Any CPU - {674B69B8-0780-4D54-AE2B-C15821FA51CB}.Release|x86.Build.0 = Release|Any CPU + {674B69B8-0780-4D54-AE2B-C15821FA51CB}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {674B69B8-0780-4D54-AE2B-C15821FA51CB}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU {1CE2D235-8072-4649-BA5A-CFB1AF8776E0}.Debug|Any CPU.ActiveCfg = Release|Any CPU {1CE2D235-8072-4649-BA5A-CFB1AF8776E0}.Debug|Any CPU.Build.0 = Release|Any CPU - {1CE2D235-8072-4649-BA5A-CFB1AF8776E0}.Debug|x86.ActiveCfg = Debug|Any CPU - {1CE2D235-8072-4649-BA5A-CFB1AF8776E0}.Debug|x86.Build.0 = Debug|Any CPU - {1CE2D235-8072-4649-BA5A-CFB1AF8776E0}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {1CE2D235-8072-4649-BA5A-CFB1AF8776E0}.Publish|x86.ActiveCfg = Release|Any CPU - {1CE2D235-8072-4649-BA5A-CFB1AF8776E0}.Publish|x86.Build.0 = Release|Any CPU + {1CE2D235-8072-4649-BA5A-CFB1AF8776E0}.DebugCodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU + {1CE2D235-8072-4649-BA5A-CFB1AF8776E0}.DebugCodeAnalysis|Any CPU.Build.0 = Release|Any CPU + {1CE2D235-8072-4649-BA5A-CFB1AF8776E0}.DebugWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {1CE2D235-8072-4649-BA5A-CFB1AF8776E0}.DebugWithoutVsix|Any CPU.Build.0 = Release|Any CPU {1CE2D235-8072-4649-BA5A-CFB1AF8776E0}.Release|Any CPU.ActiveCfg = Release|Any CPU {1CE2D235-8072-4649-BA5A-CFB1AF8776E0}.Release|Any CPU.Build.0 = Release|Any CPU - {1CE2D235-8072-4649-BA5A-CFB1AF8776E0}.Release|x86.ActiveCfg = Release|Any CPU - {1CE2D235-8072-4649-BA5A-CFB1AF8776E0}.Release|x86.Build.0 = Release|Any CPU + {1CE2D235-8072-4649-BA5A-CFB1AF8776E0}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {1CE2D235-8072-4649-BA5A-CFB1AF8776E0}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU {600998C4-54DD-4755-BFA8-6F44544D8E2E}.Debug|Any CPU.ActiveCfg = Release|Any CPU {600998C4-54DD-4755-BFA8-6F44544D8E2E}.Debug|Any CPU.Build.0 = Release|Any CPU - {600998C4-54DD-4755-BFA8-6F44544D8E2E}.Debug|x86.ActiveCfg = Debug|Any CPU - {600998C4-54DD-4755-BFA8-6F44544D8E2E}.Debug|x86.Build.0 = Debug|Any CPU - {600998C4-54DD-4755-BFA8-6F44544D8E2E}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {600998C4-54DD-4755-BFA8-6F44544D8E2E}.Publish|x86.ActiveCfg = Release|Any CPU - {600998C4-54DD-4755-BFA8-6F44544D8E2E}.Publish|x86.Build.0 = Release|Any CPU + {600998C4-54DD-4755-BFA8-6F44544D8E2E}.DebugCodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU + {600998C4-54DD-4755-BFA8-6F44544D8E2E}.DebugCodeAnalysis|Any CPU.Build.0 = Release|Any CPU + {600998C4-54DD-4755-BFA8-6F44544D8E2E}.DebugWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {600998C4-54DD-4755-BFA8-6F44544D8E2E}.DebugWithoutVsix|Any CPU.Build.0 = Release|Any CPU {600998C4-54DD-4755-BFA8-6F44544D8E2E}.Release|Any CPU.ActiveCfg = Release|Any CPU {600998C4-54DD-4755-BFA8-6F44544D8E2E}.Release|Any CPU.Build.0 = Release|Any CPU - {600998C4-54DD-4755-BFA8-6F44544D8E2E}.Release|x86.ActiveCfg = Release|Any CPU - {600998C4-54DD-4755-BFA8-6F44544D8E2E}.Release|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 = Debug|Any CPU - {3D4AE5F9-A535-4D5C-8F30-1A35D7BA0A3D}.Debug|x86.Build.0 = Debug|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 + {600998C4-54DD-4755-BFA8-6F44544D8E2E}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {600998C4-54DD-4755-BFA8-6F44544D8E2E}.ReleaseWithoutVsix|Any CPU.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 = Debug|Any CPU - {B4E665E5-6CAF-4414-A6E2-8DE1C3BCF203}.Debug|x86.Build.0 = Debug|Any CPU - {B4E665E5-6CAF-4414-A6E2-8DE1C3BCF203}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {B4E665E5-6CAF-4414-A6E2-8DE1C3BCF203}.Publish|x86.ActiveCfg = Release|Any CPU - {B4E665E5-6CAF-4414-A6E2-8DE1C3BCF203}.Publish|x86.Build.0 = Release|Any CPU + {B4E665E5-6CAF-4414-A6E2-8DE1C3BCF203}.DebugCodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU + {B4E665E5-6CAF-4414-A6E2-8DE1C3BCF203}.DebugCodeAnalysis|Any CPU.Build.0 = Release|Any CPU + {B4E665E5-6CAF-4414-A6E2-8DE1C3BCF203}.DebugWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {B4E665E5-6CAF-4414-A6E2-8DE1C3BCF203}.DebugWithoutVsix|Any CPU.Build.0 = Release|Any CPU {B4E665E5-6CAF-4414-A6E2-8DE1C3BCF203}.Release|Any CPU.ActiveCfg = Release|Any CPU {B4E665E5-6CAF-4414-A6E2-8DE1C3BCF203}.Release|Any CPU.Build.0 = Release|Any CPU - {B4E665E5-6CAF-4414-A6E2-8DE1C3BCF203}.Release|x86.ActiveCfg = Release|Any CPU - {B4E665E5-6CAF-4414-A6E2-8DE1C3BCF203}.Release|x86.Build.0 = Release|Any CPU + {B4E665E5-6CAF-4414-A6E2-8DE1C3BCF203}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {B4E665E5-6CAF-4414-A6E2-8DE1C3BCF203}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU {241C47DF-CA8E-4296-AA03-2C48BB646ABD}.Debug|Any CPU.ActiveCfg = Release|Any CPU {241C47DF-CA8E-4296-AA03-2C48BB646ABD}.Debug|Any CPU.Build.0 = Release|Any CPU - {241C47DF-CA8E-4296-AA03-2C48BB646ABD}.Debug|x86.ActiveCfg = Debug|Any CPU - {241C47DF-CA8E-4296-AA03-2C48BB646ABD}.Debug|x86.Build.0 = Debug|Any CPU - {241C47DF-CA8E-4296-AA03-2C48BB646ABD}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {241C47DF-CA8E-4296-AA03-2C48BB646ABD}.Publish|x86.ActiveCfg = Release|Any CPU - {241C47DF-CA8E-4296-AA03-2C48BB646ABD}.Publish|x86.Build.0 = Release|Any CPU + {241C47DF-CA8E-4296-AA03-2C48BB646ABD}.DebugCodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU + {241C47DF-CA8E-4296-AA03-2C48BB646ABD}.DebugCodeAnalysis|Any CPU.Build.0 = Release|Any CPU + {241C47DF-CA8E-4296-AA03-2C48BB646ABD}.DebugWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {241C47DF-CA8E-4296-AA03-2C48BB646ABD}.DebugWithoutVsix|Any CPU.Build.0 = Release|Any CPU {241C47DF-CA8E-4296-AA03-2C48BB646ABD}.Release|Any CPU.ActiveCfg = Release|Any CPU {241C47DF-CA8E-4296-AA03-2C48BB646ABD}.Release|Any CPU.Build.0 = Release|Any CPU - {241C47DF-CA8E-4296-AA03-2C48BB646ABD}.Release|x86.ActiveCfg = Release|Any CPU - {241C47DF-CA8E-4296-AA03-2C48BB646ABD}.Release|x86.Build.0 = Release|Any CPU - {EB73ADDD-2FE9-44C0-A1AB-20709B979B64}.Debug|Any CPU.ActiveCfg = Release|Any CPU - {EB73ADDD-2FE9-44C0-A1AB-20709B979B64}.Debug|Any CPU.Build.0 = Release|Any CPU - {EB73ADDD-2FE9-44C0-A1AB-20709B979B64}.Debug|x86.ActiveCfg = Debug|Any CPU - {EB73ADDD-2FE9-44C0-A1AB-20709B979B64}.Debug|x86.Build.0 = Debug|Any CPU - {EB73ADDD-2FE9-44C0-A1AB-20709B979B64}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {EB73ADDD-2FE9-44C0-A1AB-20709B979B64}.Publish|Any CPU.Build.0 = Release|Any CPU - {EB73ADDD-2FE9-44C0-A1AB-20709B979B64}.Publish|x86.ActiveCfg = Release|Any CPU - {EB73ADDD-2FE9-44C0-A1AB-20709B979B64}.Publish|x86.Build.0 = Release|Any CPU - {EB73ADDD-2FE9-44C0-A1AB-20709B979B64}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EB73ADDD-2FE9-44C0-A1AB-20709B979B64}.Release|Any CPU.Build.0 = Release|Any CPU - {EB73ADDD-2FE9-44C0-A1AB-20709B979B64}.Release|x86.ActiveCfg = Release|Any CPU - {EB73ADDD-2FE9-44C0-A1AB-20709B979B64}.Release|x86.Build.0 = Release|Any CPU + {241C47DF-CA8E-4296-AA03-2C48BB646ABD}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {241C47DF-CA8E-4296-AA03-2C48BB646ABD}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU {252CE1C2-027A-4445-A3C2-E4D6C80A935A}.Debug|Any CPU.ActiveCfg = Release|Any CPU {252CE1C2-027A-4445-A3C2-E4D6C80A935A}.Debug|Any CPU.Build.0 = Release|Any CPU - {252CE1C2-027A-4445-A3C2-E4D6C80A935A}.Debug|x86.ActiveCfg = Debug|Any CPU - {252CE1C2-027A-4445-A3C2-E4D6C80A935A}.Debug|x86.Build.0 = Debug|Any CPU - {252CE1C2-027A-4445-A3C2-E4D6C80A935A}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {252CE1C2-027A-4445-A3C2-E4D6C80A935A}.Publish|x86.ActiveCfg = Release|Any CPU - {252CE1C2-027A-4445-A3C2-E4D6C80A935A}.Publish|x86.Build.0 = Release|Any CPU + {252CE1C2-027A-4445-A3C2-E4D6C80A935A}.DebugCodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU + {252CE1C2-027A-4445-A3C2-E4D6C80A935A}.DebugCodeAnalysis|Any CPU.Build.0 = Release|Any CPU + {252CE1C2-027A-4445-A3C2-E4D6C80A935A}.DebugWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {252CE1C2-027A-4445-A3C2-E4D6C80A935A}.DebugWithoutVsix|Any CPU.Build.0 = Release|Any CPU {252CE1C2-027A-4445-A3C2-E4D6C80A935A}.Release|Any CPU.ActiveCfg = Release|Any CPU {252CE1C2-027A-4445-A3C2-E4D6C80A935A}.Release|Any CPU.Build.0 = Release|Any CPU - {252CE1C2-027A-4445-A3C2-E4D6C80A935A}.Release|x86.ActiveCfg = Release|Any CPU - {252CE1C2-027A-4445-A3C2-E4D6C80A935A}.Release|x86.Build.0 = Release|Any CPU - {0EC8DBA1-D745-4EE5-993A-6026440EC3BF}.Debug|Any CPU.ActiveCfg = Release|Any CPU - {0EC8DBA1-D745-4EE5-993A-6026440EC3BF}.Debug|Any CPU.Build.0 = Release|Any CPU - {0EC8DBA1-D745-4EE5-993A-6026440EC3BF}.Debug|x86.ActiveCfg = Debug|Any CPU - {0EC8DBA1-D745-4EE5-993A-6026440EC3BF}.Debug|x86.Build.0 = Debug|Any CPU - {0EC8DBA1-D745-4EE5-993A-6026440EC3BF}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {0EC8DBA1-D745-4EE5-993A-6026440EC3BF}.Publish|x86.ActiveCfg = Release|Any CPU - {0EC8DBA1-D745-4EE5-993A-6026440EC3BF}.Publish|x86.Build.0 = Release|Any CPU - {0EC8DBA1-D745-4EE5-993A-6026440EC3BF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0EC8DBA1-D745-4EE5-993A-6026440EC3BF}.Release|Any CPU.Build.0 = Release|Any CPU - {0EC8DBA1-D745-4EE5-993A-6026440EC3BF}.Release|x86.ActiveCfg = Release|Any CPU - {0EC8DBA1-D745-4EE5-993A-6026440EC3BF}.Release|x86.Build.0 = Release|Any CPU + {252CE1C2-027A-4445-A3C2-E4D6C80A935A}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {252CE1C2-027A-4445-A3C2-E4D6C80A935A}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU {41A47C5B-C606-45B4-B83C-22B9239E4DA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {41A47C5B-C606-45B4-B83C-22B9239E4DA0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {41A47C5B-C606-45B4-B83C-22B9239E4DA0}.Debug|x86.ActiveCfg = Debug|Any CPU - {41A47C5B-C606-45B4-B83C-22B9239E4DA0}.Debug|x86.Build.0 = Debug|Any CPU - {41A47C5B-C606-45B4-B83C-22B9239E4DA0}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {41A47C5B-C606-45B4-B83C-22B9239E4DA0}.Publish|Any CPU.Build.0 = Release|Any CPU - {41A47C5B-C606-45B4-B83C-22B9239E4DA0}.Publish|x86.ActiveCfg = Release|Any CPU - {41A47C5B-C606-45B4-B83C-22B9239E4DA0}.Publish|x86.Build.0 = Release|Any CPU + {41A47C5B-C606-45B4-B83C-22B9239E4DA0}.DebugCodeAnalysis|Any CPU.ActiveCfg = DebugCodeAnalysis|Any CPU + {41A47C5B-C606-45B4-B83C-22B9239E4DA0}.DebugCodeAnalysis|Any CPU.Build.0 = DebugCodeAnalysis|Any CPU + {41A47C5B-C606-45B4-B83C-22B9239E4DA0}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {41A47C5B-C606-45B4-B83C-22B9239E4DA0}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU {41A47C5B-C606-45B4-B83C-22B9239E4DA0}.Release|Any CPU.ActiveCfg = Release|Any CPU {41A47C5B-C606-45B4-B83C-22B9239E4DA0}.Release|Any CPU.Build.0 = Release|Any CPU - {41A47C5B-C606-45B4-B83C-22B9239E4DA0}.Release|x86.ActiveCfg = Release|Any CPU - {41A47C5B-C606-45B4-B83C-22B9239E4DA0}.Release|x86.Build.0 = Release|Any CPU - {DD99FD0F-82F6-4C30-930E-4A1D0DF01D65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DD99FD0F-82F6-4C30-930E-4A1D0DF01D65}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DD99FD0F-82F6-4C30-930E-4A1D0DF01D65}.Debug|x86.ActiveCfg = Debug|Any CPU - {DD99FD0F-82F6-4C30-930E-4A1D0DF01D65}.Debug|x86.Build.0 = Debug|Any CPU - {DD99FD0F-82F6-4C30-930E-4A1D0DF01D65}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {DD99FD0F-82F6-4C30-930E-4A1D0DF01D65}.Publish|Any CPU.Build.0 = Release|Any CPU - {DD99FD0F-82F6-4C30-930E-4A1D0DF01D65}.Publish|x86.ActiveCfg = Release|Any CPU - {DD99FD0F-82F6-4C30-930E-4A1D0DF01D65}.Publish|x86.Build.0 = Release|Any CPU - {DD99FD0F-82F6-4C30-930E-4A1D0DF01D65}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DD99FD0F-82F6-4C30-930E-4A1D0DF01D65}.Release|Any CPU.Build.0 = Release|Any CPU - {DD99FD0F-82F6-4C30-930E-4A1D0DF01D65}.Release|x86.ActiveCfg = Release|Any CPU - {DD99FD0F-82F6-4C30-930E-4A1D0DF01D65}.Release|x86.Build.0 = Release|Any CPU + {41A47C5B-C606-45B4-B83C-22B9239E4DA0}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {41A47C5B-C606-45B4-B83C-22B9239E4DA0}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU {7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Debug|x86.ActiveCfg = Debug|Any CPU - {7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Debug|x86.Build.0 = Debug|Any CPU - {7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Publish|Any CPU.ActiveCfg = Release|Any CPU - {7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Publish|Any CPU.Build.0 = Release|Any CPU - {7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Publish|x86.ActiveCfg = Release|Any CPU - {7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Publish|x86.Build.0 = Release|Any CPU + {7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.DebugCodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU + {7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.DebugCodeAnalysis|Any CPU.Build.0 = Debug|Any CPU + {7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU {7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Release|Any CPU.ActiveCfg = Release|Any CPU {7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Release|Any CPU.Build.0 = Release|Any CPU - {7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Release|x86.ActiveCfg = Release|Any CPU - {7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.Release|x86.Build.0 = Release|Any CPU + {7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {7B835A7D-CF94-45E8-B191-96F5A4FE26A8}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU + {161DBF01-1DBF-4B00-8551-C5C00F26720D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {161DBF01-1DBF-4B00-8551-C5C00F26720D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {161DBF01-1DBF-4B00-8551-C5C00F26720D}.DebugCodeAnalysis|Any CPU.ActiveCfg = DebugCodeAnalysis|Any CPU + {161DBF01-1DBF-4B00-8551-C5C00F26720D}.DebugCodeAnalysis|Any CPU.Build.0 = DebugCodeAnalysis|Any CPU + {161DBF01-1DBF-4B00-8551-C5C00F26720D}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {161DBF01-1DBF-4B00-8551-C5C00F26720D}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU + {161DBF01-1DBF-4B00-8551-C5C00F26720D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {161DBF01-1DBF-4B00-8551-C5C00F26720D}.Release|Any CPU.Build.0 = Release|Any CPU + {161DBF01-1DBF-4B00-8551-C5C00F26720D}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {161DBF01-1DBF-4B00-8551-C5C00F26720D}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU + {161DBF01-1DBF-4B00-8551-C5C00F26720E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {161DBF01-1DBF-4B00-8551-C5C00F26720E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {161DBF01-1DBF-4B00-8551-C5C00F26720E}.DebugCodeAnalysis|Any CPU.ActiveCfg = DebugCodeAnalysis|Any CPU + {161DBF01-1DBF-4B00-8551-C5C00F26720E}.DebugCodeAnalysis|Any CPU.Build.0 = DebugCodeAnalysis|Any CPU + {161DBF01-1DBF-4B00-8551-C5C00F26720E}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {161DBF01-1DBF-4B00-8551-C5C00F26720E}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU + {161DBF01-1DBF-4B00-8551-C5C00F26720E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {161DBF01-1DBF-4B00-8551-C5C00F26720E}.Release|Any CPU.Build.0 = Release|Any CPU + {161DBF01-1DBF-4B00-8551-C5C00F26720E}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {161DBF01-1DBF-4B00-8551-C5C00F26720E}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU + {D1DFBB0C-B570-4302-8F1E-2E3A19C41961}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1DFBB0C-B570-4302-8F1E-2E3A19C41961}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1DFBB0C-B570-4302-8F1E-2E3A19C41961}.DebugCodeAnalysis|Any CPU.ActiveCfg = DebugCodeAnalysis|Any CPU + {D1DFBB0C-B570-4302-8F1E-2E3A19C41961}.DebugCodeAnalysis|Any CPU.Build.0 = DebugCodeAnalysis|Any CPU + {D1DFBB0C-B570-4302-8F1E-2E3A19C41961}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {D1DFBB0C-B570-4302-8F1E-2E3A19C41961}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU + {D1DFBB0C-B570-4302-8F1E-2E3A19C41961}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1DFBB0C-B570-4302-8F1E-2E3A19C41961}.Release|Any CPU.Build.0 = Release|Any CPU + {D1DFBB0C-B570-4302-8F1E-2E3A19C41961}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {D1DFBB0C-B570-4302-8F1E-2E3A19C41961}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU + {50E277B8-8580-487A-8F8E-5C3B9FBF0F77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50E277B8-8580-487A-8F8E-5C3B9FBF0F77}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50E277B8-8580-487A-8F8E-5C3B9FBF0F77}.DebugCodeAnalysis|Any CPU.ActiveCfg = DebugCodeAnalysis|Any CPU + {50E277B8-8580-487A-8F8E-5C3B9FBF0F77}.DebugCodeAnalysis|Any CPU.Build.0 = DebugCodeAnalysis|Any CPU + {50E277B8-8580-487A-8F8E-5C3B9FBF0F77}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {50E277B8-8580-487A-8F8E-5C3B9FBF0F77}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU + {50E277B8-8580-487A-8F8E-5C3B9FBF0F77}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50E277B8-8580-487A-8F8E-5C3B9FBF0F77}.Release|Any CPU.Build.0 = Release|Any CPU + {50E277B8-8580-487A-8F8E-5C3B9FBF0F77}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {50E277B8-8580-487A-8F8E-5C3B9FBF0F77}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU + {110B206F-8554-4B51-BF86-94DAA32F5E26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {110B206F-8554-4B51-BF86-94DAA32F5E26}.Debug|Any CPU.Build.0 = Debug|Any CPU + {110B206F-8554-4B51-BF86-94DAA32F5E26}.DebugCodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU + {110B206F-8554-4B51-BF86-94DAA32F5E26}.DebugCodeAnalysis|Any CPU.Build.0 = Debug|Any CPU + {110B206F-8554-4B51-BF86-94DAA32F5E26}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {110B206F-8554-4B51-BF86-94DAA32F5E26}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU + {110B206F-8554-4B51-BF86-94DAA32F5E26}.Release|Any CPU.ActiveCfg = Release|Any CPU + {110B206F-8554-4B51-BF86-94DAA32F5E26}.Release|Any CPU.Build.0 = Release|Any CPU + {110B206F-8554-4B51-BF86-94DAA32F5E26}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {110B206F-8554-4B51-BF86-94DAA32F5E26}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU + {7F5ED78B-74A3-4406-A299-70CFB5885B8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F5ED78B-74A3-4406-A299-70CFB5885B8B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F5ED78B-74A3-4406-A299-70CFB5885B8B}.DebugCodeAnalysis|Any CPU.ActiveCfg = DebugCodeAnalysis|Any CPU + {7F5ED78B-74A3-4406-A299-70CFB5885B8B}.DebugCodeAnalysis|Any CPU.Build.0 = DebugCodeAnalysis|Any CPU + {7F5ED78B-74A3-4406-A299-70CFB5885B8B}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {7F5ED78B-74A3-4406-A299-70CFB5885B8B}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU + {7F5ED78B-74A3-4406-A299-70CFB5885B8B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F5ED78B-74A3-4406-A299-70CFB5885B8B}.Release|Any CPU.Build.0 = Release|Any CPU + {7F5ED78B-74A3-4406-A299-70CFB5885B8B}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {7F5ED78B-74A3-4406-A299-70CFB5885B8B}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU + {17EB676B-BB91-48B5-AA59-C67695C647C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17EB676B-BB91-48B5-AA59-C67695C647C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17EB676B-BB91-48B5-AA59-C67695C647C2}.DebugCodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU + {17EB676B-BB91-48B5-AA59-C67695C647C2}.DebugCodeAnalysis|Any CPU.Build.0 = Debug|Any CPU + {17EB676B-BB91-48B5-AA59-C67695C647C2}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {17EB676B-BB91-48B5-AA59-C67695C647C2}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU + {17EB676B-BB91-48B5-AA59-C67695C647C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17EB676B-BB91-48B5-AA59-C67695C647C2}.Release|Any CPU.Build.0 = Release|Any CPU + {17EB676B-BB91-48B5-AA59-C67695C647C2}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {17EB676B-BB91-48B5-AA59-C67695C647C2}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU + {8D73575A-A89F-47CC-B153-B47DD06837F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D73575A-A89F-47CC-B153-B47DD06837F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D73575A-A89F-47CC-B153-B47DD06837F0}.DebugCodeAnalysis|Any CPU.ActiveCfg = DebugCodeAnalysis|Any CPU + {8D73575A-A89F-47CC-B153-B47DD06837F0}.DebugCodeAnalysis|Any CPU.Build.0 = DebugCodeAnalysis|Any CPU + {8D73575A-A89F-47CC-B153-B47DD06837F0}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {8D73575A-A89F-47CC-B153-B47DD06837F0}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU + {8D73575A-A89F-47CC-B153-B47DD06837F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D73575A-A89F-47CC-B153-B47DD06837F0}.Release|Any CPU.Build.0 = Release|Any CPU + {8D73575A-A89F-47CC-B153-B47DD06837F0}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {8D73575A-A89F-47CC-B153-B47DD06837F0}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU + {2D3D2834-33BE-45CA-B3CC-12F853557D7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D3D2834-33BE-45CA-B3CC-12F853557D7B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D3D2834-33BE-45CA-B3CC-12F853557D7B}.DebugCodeAnalysis|Any CPU.ActiveCfg = DebugCodeAnalysis|Any CPU + {2D3D2834-33BE-45CA-B3CC-12F853557D7B}.DebugCodeAnalysis|Any CPU.Build.0 = DebugCodeAnalysis|Any CPU + {2D3D2834-33BE-45CA-B3CC-12F853557D7B}.DebugWithoutVsix|Any CPU.ActiveCfg = DebugCodeAnalysis|Any CPU + {2D3D2834-33BE-45CA-B3CC-12F853557D7B}.DebugWithoutVsix|Any CPU.Build.0 = DebugCodeAnalysis|Any CPU + {2D3D2834-33BE-45CA-B3CC-12F853557D7B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D3D2834-33BE-45CA-B3CC-12F853557D7B}.Release|Any CPU.Build.0 = Release|Any CPU + {2D3D2834-33BE-45CA-B3CC-12F853557D7B}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {2D3D2834-33BE-45CA-B3CC-12F853557D7B}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU + {14FDEE91-7301-4247-846C-049647BF8E99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14FDEE91-7301-4247-846C-049647BF8E99}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14FDEE91-7301-4247-846C-049647BF8E99}.DebugCodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU + {14FDEE91-7301-4247-846C-049647BF8E99}.DebugCodeAnalysis|Any CPU.Build.0 = Debug|Any CPU + {14FDEE91-7301-4247-846C-049647BF8E99}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {14FDEE91-7301-4247-846C-049647BF8E99}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU + {14FDEE91-7301-4247-846C-049647BF8E99}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14FDEE91-7301-4247-846C-049647BF8E99}.Release|Any CPU.Build.0 = Release|Any CPU + {14FDEE91-7301-4247-846C-049647BF8E99}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {14FDEE91-7301-4247-846C-049647BF8E99}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU + {09313E65-7ADB-48C1-AD3A-572020C5BDCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09313E65-7ADB-48C1-AD3A-572020C5BDCB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09313E65-7ADB-48C1-AD3A-572020C5BDCB}.DebugCodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU + {09313E65-7ADB-48C1-AD3A-572020C5BDCB}.DebugCodeAnalysis|Any CPU.Build.0 = Debug|Any CPU + {09313E65-7ADB-48C1-AD3A-572020C5BDCB}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {09313E65-7ADB-48C1-AD3A-572020C5BDCB}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU + {09313E65-7ADB-48C1-AD3A-572020C5BDCB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09313E65-7ADB-48C1-AD3A-572020C5BDCB}.Release|Any CPU.Build.0 = Release|Any CPU + {09313E65-7ADB-48C1-AD3A-572020C5BDCB}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {09313E65-7ADB-48C1-AD3A-572020C5BDCB}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU + {EFDE0798-ACDB-431D-B7F1-548A7231C853}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EFDE0798-ACDB-431D-B7F1-548A7231C853}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFDE0798-ACDB-431D-B7F1-548A7231C853}.DebugCodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU + {EFDE0798-ACDB-431D-B7F1-548A7231C853}.DebugCodeAnalysis|Any CPU.Build.0 = Debug|Any CPU + {EFDE0798-ACDB-431D-B7F1-548A7231C853}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {EFDE0798-ACDB-431D-B7F1-548A7231C853}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU + {EFDE0798-ACDB-431D-B7F1-548A7231C853}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EFDE0798-ACDB-431D-B7F1-548A7231C853}.Release|Any CPU.Build.0 = Release|Any CPU + {EFDE0798-ACDB-431D-B7F1-548A7231C853}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {EFDE0798-ACDB-431D-B7F1-548A7231C853}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU + {3525D819-6AEC-4879-89FB-56B41F026571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3525D819-6AEC-4879-89FB-56B41F026571}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3525D819-6AEC-4879-89FB-56B41F026571}.DebugCodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU + {3525D819-6AEC-4879-89FB-56B41F026571}.DebugCodeAnalysis|Any CPU.Build.0 = Debug|Any CPU + {3525D819-6AEC-4879-89FB-56B41F026571}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {3525D819-6AEC-4879-89FB-56B41F026571}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU + {3525D819-6AEC-4879-89FB-56B41F026571}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3525D819-6AEC-4879-89FB-56B41F026571}.Release|Any CPU.Build.0 = Release|Any CPU + {3525D819-6AEC-4879-89FB-56B41F026571}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {3525D819-6AEC-4879-89FB-56B41F026571}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU + {94509FCB-6C97-4ED6-AED6-6E74AB3CA336}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {94509FCB-6C97-4ED6-AED6-6E74AB3CA336}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94509FCB-6C97-4ED6-AED6-6E74AB3CA336}.DebugCodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU + {94509FCB-6C97-4ED6-AED6-6E74AB3CA336}.DebugCodeAnalysis|Any CPU.Build.0 = Debug|Any CPU + {94509FCB-6C97-4ED6-AED6-6E74AB3CA336}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {94509FCB-6C97-4ED6-AED6-6E74AB3CA336}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU + {94509FCB-6C97-4ED6-AED6-6E74AB3CA336}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94509FCB-6C97-4ED6-AED6-6E74AB3CA336}.Release|Any CPU.Build.0 = Release|Any CPU + {94509FCB-6C97-4ED6-AED6-6E74AB3CA336}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {94509FCB-6C97-4ED6-AED6-6E74AB3CA336}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU + {C59868FC-D8BC-4D47-B4F3-16908D2641C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C59868FC-D8BC-4D47-B4F3-16908D2641C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C59868FC-D8BC-4D47-B4F3-16908D2641C6}.DebugCodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU + {C59868FC-D8BC-4D47-B4F3-16908D2641C6}.DebugCodeAnalysis|Any CPU.Build.0 = Debug|Any CPU + {C59868FC-D8BC-4D47-B4F3-16908D2641C6}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {C59868FC-D8BC-4D47-B4F3-16908D2641C6}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU + {C59868FC-D8BC-4D47-B4F3-16908D2641C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C59868FC-D8BC-4D47-B4F3-16908D2641C6}.Release|Any CPU.Build.0 = Release|Any CPU + {C59868FC-D8BC-4D47-B4F3-16908D2641C6}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {C59868FC-D8BC-4D47-B4F3-16908D2641C6}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU + {DE704BBB-6EC6-4173-B695-D9EBF5AEB092}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE704BBB-6EC6-4173-B695-D9EBF5AEB092}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE704BBB-6EC6-4173-B695-D9EBF5AEB092}.DebugCodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU + {DE704BBB-6EC6-4173-B695-D9EBF5AEB092}.DebugCodeAnalysis|Any CPU.Build.0 = Debug|Any CPU + {DE704BBB-6EC6-4173-B695-D9EBF5AEB092}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {DE704BBB-6EC6-4173-B695-D9EBF5AEB092}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU + {DE704BBB-6EC6-4173-B695-D9EBF5AEB092}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE704BBB-6EC6-4173-B695-D9EBF5AEB092}.Release|Any CPU.Build.0 = Release|Any CPU + {DE704BBB-6EC6-4173-B695-D9EBF5AEB092}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {DE704BBB-6EC6-4173-B695-D9EBF5AEB092}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU + {E687457A-BEDC-422D-8D9D-2DA58099EBBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E687457A-BEDC-422D-8D9D-2DA58099EBBA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E687457A-BEDC-422D-8D9D-2DA58099EBBA}.DebugCodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU + {E687457A-BEDC-422D-8D9D-2DA58099EBBA}.DebugCodeAnalysis|Any CPU.Build.0 = Debug|Any CPU + {E687457A-BEDC-422D-8D9D-2DA58099EBBA}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {E687457A-BEDC-422D-8D9D-2DA58099EBBA}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU + {E687457A-BEDC-422D-8D9D-2DA58099EBBA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E687457A-BEDC-422D-8D9D-2DA58099EBBA}.Release|Any CPU.Build.0 = Release|Any CPU + {E687457A-BEDC-422D-8D9D-2DA58099EBBA}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {E687457A-BEDC-422D-8D9D-2DA58099EBBA}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU + {93778A89-3E58-4853-B772-948EBB3F17BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {93778A89-3E58-4853-B772-948EBB3F17BE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {93778A89-3E58-4853-B772-948EBB3F17BE}.DebugCodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU + {93778A89-3E58-4853-B772-948EBB3F17BE}.DebugCodeAnalysis|Any CPU.Build.0 = Debug|Any CPU + {93778A89-3E58-4853-B772-948EBB3F17BE}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {93778A89-3E58-4853-B772-948EBB3F17BE}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU + {93778A89-3E58-4853-B772-948EBB3F17BE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {93778A89-3E58-4853-B772-948EBB3F17BE}.Release|Any CPU.Build.0 = Release|Any CPU + {93778A89-3E58-4853-B772-948EBB3F17BE}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {93778A89-3E58-4853-B772-948EBB3F17BE}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU + {8B14F90B-0781-465D-AB94-19C8C56E3A94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B14F90B-0781-465D-AB94-19C8C56E3A94}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B14F90B-0781-465D-AB94-19C8C56E3A94}.DebugCodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU + {8B14F90B-0781-465D-AB94-19C8C56E3A94}.DebugCodeAnalysis|Any CPU.Build.0 = Debug|Any CPU + {8B14F90B-0781-465D-AB94-19C8C56E3A94}.DebugWithoutVsix|Any CPU.ActiveCfg = Debug|Any CPU + {8B14F90B-0781-465D-AB94-19C8C56E3A94}.DebugWithoutVsix|Any CPU.Build.0 = Debug|Any CPU + {8B14F90B-0781-465D-AB94-19C8C56E3A94}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B14F90B-0781-465D-AB94-19C8C56E3A94}.Release|Any CPU.Build.0 = Release|Any CPU + {8B14F90B-0781-465D-AB94-19C8C56E3A94}.ReleaseWithoutVsix|Any CPU.ActiveCfg = Release|Any CPU + {8B14F90B-0781-465D-AB94-19C8C56E3A94}.ReleaseWithoutVsix|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {596595A6-2A3C-469E-9386-9E3767D863A5} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD} - {4A84E568-CA86-4510-8CD0-90D3EF9B65F9} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AB8} - {8446C785-A5B4-4676-9B38-560FCA0563E0} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AB8} - {EE6ED99F-CB12-4683-B055-D28FC7357A34} = {8446C785-A5B4-4676-9B38-560FCA0563E0} {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} - {EB73ADDD-2FE9-44C0-A1AB-20709B979B64} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AC9} {1E7F7253-A6AF-43C4-A955-37BEDDA01AF9} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AB8} {252CE1C2-027A-4445-A3C2-E4D6C80A935A} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AF9} - {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} + {110B206F-8554-4B51-BF86-94DAA32F5E26} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD} + {17EB676B-BB91-48B5-AA59-C67695C647C2} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD} + {C2D88962-BD6B-4F11-B914-535B38377962} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD} + {14FDEE91-7301-4247-846C-049647BF8E99} = {C2D88962-BD6B-4F11-B914-535B38377962} + {09313E65-7ADB-48C1-AD3A-572020C5BDCB} = {C2D88962-BD6B-4F11-B914-535B38377962} + {EFDE0798-ACDB-431D-B7F1-548A7231C853} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD} + {3525D819-6AEC-4879-89FB-56B41F026571} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD} + {94509FCB-6C97-4ED6-AED6-6E74AB3CA336} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD} + {C59868FC-D8BC-4D47-B4F3-16908D2641C6} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD} + {DE704BBB-6EC6-4173-B695-D9EBF5AEB092} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD} + {E687457A-BEDC-422D-8D9D-2DA58099EBBA} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD} + {93778A89-3E58-4853-B772-948EBB3F17BE} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD} + {8B14F90B-0781-465D-AB94-19C8C56E3A94} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {556014CF-5B35-4CE5-B3EF-6AB0007001AC} EndGlobalSection EndGlobal diff --git a/GitHubVS.v3.ncrunchsolution b/GitHubVS.v3.ncrunchsolution new file mode 100644 index 0000000000..2cd8e79788 --- /dev/null +++ b/GitHubVS.v3.ncrunchsolution @@ -0,0 +1,14 @@ + + + + packages\Fody.1.29.4\**.* + packages\EntryExitDecorator.Fody.0.3.0\**.* + + + lib\**.* + + False + .ncrunch + True + + \ No newline at end of file diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..2c895f02d6 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,24 @@ + + +## Version + +- GitHub Extension for Visual Studio version: +- Visual Studio version: + +## What happened + +**Steps to Reproduce** + +1. [First Step] +2. [Second Step] +3. [and so on...] + +**Expected behavior:** [What you expect to happen] + +**Actual behavior:** [What actually happens] + +**Screenshot or GIF:** + +**Log file:** + + diff --git a/LICENSE.md b/LICENSE.md index 0b506974f9..a9872c8b69 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2015 GitHub Inc. +Copyright (c) GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index f6a804ccf2..9eedee0630 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,49 @@ # GitHub Extension for Visual Studio +## Notices + +### If you are having issues with the installer, please read + +If you need to upgrade, downgrade, or uninstall the extension, and are having problems doing so, refer to this issue: https://github.com/github/VisualStudio/issues/1394 which details common problems and solutions when using the installer. + +### The location of the submodules has changed as of 31-01-2017 + +If you have an existing clone, make sure to run `git submodule sync` to update your local clone with the new locations for the submodules. + +## About + The GitHub Extension for Visual Studio provides GitHub integration in Visual Studio 2015. Most of the extension UI lives in the Team Explorer pane, which is available from the View menu. Official builds of this extension are available at [the official website](https://visualstudio.github.com). -[![Join the chat at https://gitter.im/github/VisualStudio](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/github/VisualStudio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +[![Build status](https://ci.appveyor.com/api/projects/status/dl8is5iqwt9qf3t7/branch/master?svg=true)](https://ci.appveyor.com/project/github-windows/visualstudio/branch/master) +[![codecov](https://codecov.io/gh/GitHub/VisualStudio/branch/master/graph/badge.svg)](https://codecov.io/gh/GitHub/VisualStudio) + +[![Join the chat at freenode:github-vs](https://img.shields.io/badge/irc-freenode:%20%23github--vs-blue.svg)](http://webchat.freenode.net/?channels=%23github-vs) [![Join the chat at https://gitter.im/github/VisualStudio](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/github/VisualStudio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +## Documentation +Visit the [documentation](https://github.com/github/VisualStudio/tree/master/docs) for details on how to use the features in the GitHub Extension for Visual Studio. + +## Installing beta versions + +Older and pre-release/beta/untested versions are available at [the releases page](https://github.com/github/VisualStudio/releases), and also via a custom gallery feed for Visual Studio. + +You can configure the gallery by going to `Tools / Options / Extensions and Updates` and adding a new gallery with the url https://visualstudio.github.com/releases/feed.rss. The gallery will now be available from `Tools / Extensions and Updates`. + +Beta releases will have `(beta)` in their title in the gallery, following the version number. You can view the release notes in the gallery by hovering over the description, or by clicking the `Release Notes` link on the right side. ## Build requirements -* Visual Studio 2015 +* Visual Studio 2017 (15.7.4)+ * Visual Studio SDK ## Build Clone the repository and its submodules in a git GUI client or via the command line: -``` +```txt git clone https://github.com/github/VisualStudio cd VisualStudio git submodule init @@ -24,11 +51,48 @@ git submodule deinit script git submodule update ``` -Open the `GitHubVS.sln` solution with Visual Studio 2015. +Open the `GitHubVS.sln` solution with Visual Studio 2017+. To be able to use the GitHub API, you'll need to: - [Register a new developer application](https://github.com/settings/developers) in your profile. -- Open [src/GitHub.App/Api/ApiClientConfiguration.cs](src/GitHub.App/Api/ApiClientConfiguration.cs) and fill out the clientId/clientSecret fields for your application. +- Open [src/GitHub.Api/ApiClientConfiguration_User.cs](src/GitHub.Api/ApiClientConfiguration_User.cs) and fill out the clientId/clientSecret fields for your application. **Note this has recently changed location, so you may need to re-do this** + +Build using Visual Studio 2017 or: + +```txt +build.cmd +``` + +Install in live (non-Experimental) instances of Visual Studio 2015 and 2017: + +```txt +install.cmd +``` + +Note, the script will only install in one instance of Visual Studio 2017 (Enterprise, Professional or Community). + +## Build Flavors + +The following can be executed via `cmd.exe`. + +To build and install a `Debug` configuration VSIX: +```txt +build.cmd Debug +install.cmd Debug +``` + +To build and install a `Release` configuration VSIX: +```txt +build.cmd Release +install.cmd Release +``` +## Logs +Logs can be viewed at the following location: + +`%LOCALAPPDATA%\GitHubVisualStudio\extension.log` + +## More information +- Andreia Gaita's [presentation](https://www.youtube.com/watch?v=hz2hCO8e_8w) at Codemania 2016 about this extension. ## Contributing @@ -36,6 +100,7 @@ Visit the [Contributor Guidelines](CONTRIBUTING.md) for details on how to contri ## Copyright -Copyright 2015 GitHub, Inc. +Copyright 2015 - 2018 GitHub, Inc. Licensed under the [MIT License](LICENSE.md) + diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000000..d103ff2160 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,37 @@ +os: Visual Studio 2017 +version: '2.5.6.{build}' +skip_tags: true +install: +- ps: | + $full_build = Test-Path env:GHFVS_KEY + git submodule init + git submodule sync + + if ($full_build) { + $fileContent = "-----BEGIN RSA PRIVATE KEY-----`n" + $fileContent += $env:GHFVS_KEY.Replace(' ', "`n") + $fileContent += "`n-----END RSA PRIVATE KEY-----`n" + Set-Content c:\users\appveyor\.ssh\id_rsa $fileContent + } else { + git submodule deinit script + } + + git submodule update --recursive --force + nuget restore GitHubVS.sln +build_script: +- ps: scripts\build.ps1 -AppVeyor -BuildNumber:$env:APPVEYOR_BUILD_NUMBER +test: + categories: + except: + - Timings +after_test: +- ps: | + choco install opencover.portable codecov + OpenCover.Console.exe --% "-target:nunit3-console.exe" "-targetargs: test\GitHub.Api.UnitTests\bin\Release\GitHub.Api.UnitTests.dll test\GitHub.App.UnitTests\bin\Release\GitHub.App.UnitTests.dll test\GitHub.Exports.Reactive.UnitTests\bin\Release\GitHub.Exports.Reactive.UnitTests.dll test\GitHub.Exports.UnitTests\bin\Release\GitHub.Exports.UnitTests.dll test\GitHub.Extensions.UnitTests\bin\Release\GitHub.Extensions.UnitTests.dll test\GitHub.InlineReviews.UnitTests\bin\Release\GitHub.InlineReviews.UnitTests.dll test\GitHub.Primitives.UnitTests\bin\Release\GitHub.Primitives.UnitTests.dll test\GitHub.TeamFoundation.UnitTests\bin\Release\GitHub.TeamFoundation.UnitTests.dll test\GitHub.UI.UnitTests\bin\Release\GitHub.UI.UnitTests.dll test\GitHub.VisualStudio.UnitTests\bin\Release\GitHub.VisualStudio.UnitTests.dll test\MetricsTests\MetricsTests\bin\Release\MetricsTests.dll test\TrackingCollectionTests\bin\Release\TrackingCollectionTests.dll --where cat!=Timings" -filter:"+[GitHub*]* -[GitHub*]*UnitTests" -register:user -output:".\coverage.xml" + codecov -f "coverage.xml" + Push-AppveyorArtifact coverage.xml +on_success: +- ps: | + if ($full_build) { + script\Sign-Package -AppVeyor + } diff --git a/build.cmd b/build.cmd index 7b7f67072a..ed204a53a7 100644 --- a/build.cmd +++ b/build.cmd @@ -1 +1,2 @@ -powershell.exe .\script\cibuild.ps1 \ No newline at end of file +@if "%1" == "" echo Please specify Debug or Release && EXIT /B +powershell -ExecutionPolicy Unrestricted scripts\build.ps1 -Package:$true -Config:%1 diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..1727ea602d --- /dev/null +++ b/codecov.yml @@ -0,0 +1,29 @@ +codecov: + notify: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "70...100" + + status: + project: yes + patch: yes + changes: no + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "header, diff" + behavior: default + require_changes: no + +fixes: + - "C:\projects\visualstudio\::" diff --git a/deploy-local.cmd b/deploy-local.cmd new file mode 100644 index 0000000000..64f80141d9 --- /dev/null +++ b/deploy-local.cmd @@ -0,0 +1 @@ +powershell.exe .\script\deploy.ps1 -Force -NoChat -NoPush -NoUpload \ No newline at end of file diff --git a/deploy.cmd b/deploy.cmd deleted file mode 100644 index 0998692f9f..0000000000 --- a/deploy.cmd +++ /dev/null @@ -1 +0,0 @@ -powershell.exe .\script\deploy.ps1 staff development %1 -Force -NoCampfire \ No newline at end of file diff --git a/docs/contributing/cloning-a-repository-to-visual-studio.md b/docs/contributing/cloning-a-repository-to-visual-studio.md new file mode 100644 index 0000000000..1ccea26979 --- /dev/null +++ b/docs/contributing/cloning-a-repository-to-visual-studio.md @@ -0,0 +1,19 @@ +# Cloning a repository to Visual Studio + +After you provide your GitHub or GitHub Enterprise credentials to GitHub for Visual Studio, the extension automatically detects the personal and organization repositories you have access to on your account. + +1. Open **Team Explorer** by clicking on its tab next to *Solution Explorer*, or via the *View* menu. +2. Click the **Manage Connections** toolbar button. + +![Location of the manage connections toolbar button in Team Explorer](images/manage-connections.png) + +3. Next to the account you want to clone from, click **Clone**. + +![Clone button in the GitHub section of Team Explorer](images/clone-link.png) + +4. In the list of repositories, click the repository you'd like to clone. + +![List of GitHub repositories that can be cloned inside a dialog](images/clone-dialog.png) + +5. If desired, change the local path where the repository will be cloned into, or leave the default as-is. Click **Clone**. +6. In Team Explorer, under the list of repositories, locate the repository and double-click to open the project in Visual Studio. diff --git a/docs/contributing/creating-a-pull-request.md b/docs/contributing/creating-a-pull-request.md new file mode 100644 index 0000000000..c2eb313cd0 --- /dev/null +++ b/docs/contributing/creating-a-pull-request.md @@ -0,0 +1,10 @@ +# Creating a pull request + +1. Open a solution in a GitHub repository. +2. Open **Team Explorer** and click the **Pull Requests** button to open the **GitHub** pane. +![Location of the pull requests button in the Team Explorer pane](images/pull-requests-button2.png) +3. Click the **Create New** link above the list of pull requests for the repository. +4. Select the target branch by clicking the link. If the current repository is a fork, then there will be two sets of branches in the dropdown - to submit a pull request upstream then select a branch with the `owner:` prefix of the upstream repository. +![The pull request creation form in the GitHub pane](images/pr-create.png) +5. Enter a pull request title and an optional description. +6. Click the **Create Pull Request** button. diff --git a/docs/contributing/creating-an-empty-repository-from-visual-studio.md b/docs/contributing/creating-an-empty-repository-from-visual-studio.md new file mode 100644 index 0000000000..4252863095 --- /dev/null +++ b/docs/contributing/creating-an-empty-repository-from-visual-studio.md @@ -0,0 +1,27 @@ +# Creating an empty repository from Visual Studio + +1. [Sign in](../getting-started/authenticating-to-github.md) to GitHub. + +2. Open **Team Explorer** by clicking on its tab next to *Solution Explorer*, or via the *View* menu. + +3. Click the **Manage Connections** toolbar button. + + ![The manage connections toolbar button in Team Explorer](images/manage-connections.png) + +4. Click the **Create** link next to the account you want to create the repository in. + + ![The create link in the Team Explorer pane](images/create-link.png) + +5. In the **Create a GitHub Repository** dialog, enter a name, description and local path for the repository. + + ![The create a GitHub repository dialog](images/create-dialog.png) + +6. Select a license for the repository. + +7. Check the **Private Repository** box if you want to upload the repository as a private repository on GitHub. You must have a [Developer, Team or Business account](https://github.com/pricing) to create private repositories. + +8. Click the **Create** button to create the repository + +9. When the repository is created, click the **Create a new Project or Solution** link in Team Explorer to create a project or solution in the repository. + + ![Successful repository creation message at the top of the Team Explorer pane](images/successful-creation-message.png) diff --git a/docs/contributing/creating-gists.md b/docs/contributing/creating-gists.md new file mode 100644 index 0000000000..1a5547457b --- /dev/null +++ b/docs/contributing/creating-gists.md @@ -0,0 +1,23 @@ +# Creating gists + +GitHub for Visual Studio enables easy creation of gists directly from the Visual Studio Editor. + +1. [Sign in](../getting-started/authenticating-to-github.md) to GitHub. + +2. Open a file in the Visual Studio text editor. + +3. Select the section of text that you want to create a gist from. + +4. Right click and select **Create a GitHub Gist** from the **GitHub** submenu. + + ![Location of Create A GitHub Gist in the GitHub submenu](images/create-gist-menu.png) + +5. In the **Create a GitHub Gist** dialog, check that the filename is correct and optionally add a description. + + ![GitHub Gist creation dialog window](images/create-gist-dialog.png) + +6. If you want the gist to be private, check the **Private Gist** checkbox. + +7. Click **Create**. + +8. Once the gist is created it will be opened in your browser. diff --git a/docs/contributing/images/add-comment.png b/docs/contributing/images/add-comment.png new file mode 100644 index 0000000000..3bbc5244f4 Binary files /dev/null and b/docs/contributing/images/add-comment.png differ diff --git a/docs/contributing/images/add-to-source-control.png b/docs/contributing/images/add-to-source-control.png new file mode 100644 index 0000000000..7154c14a63 Binary files /dev/null and b/docs/contributing/images/add-to-source-control.png differ diff --git a/docs/contributing/images/clone-dialog.png b/docs/contributing/images/clone-dialog.png new file mode 100644 index 0000000000..2e44a61916 Binary files /dev/null and b/docs/contributing/images/clone-dialog.png differ diff --git a/docs/contributing/images/clone-link.png b/docs/contributing/images/clone-link.png new file mode 100644 index 0000000000..eb8a26bae8 Binary files /dev/null and b/docs/contributing/images/clone-link.png differ diff --git a/docs/contributing/images/create-dialog.png b/docs/contributing/images/create-dialog.png new file mode 100644 index 0000000000..16f3dfd013 Binary files /dev/null and b/docs/contributing/images/create-dialog.png differ diff --git a/docs/contributing/images/create-gist-dialog.png b/docs/contributing/images/create-gist-dialog.png new file mode 100644 index 0000000000..fbab9a2abf Binary files /dev/null and b/docs/contributing/images/create-gist-dialog.png differ diff --git a/docs/contributing/images/create-gist-menu.png b/docs/contributing/images/create-gist-menu.png new file mode 100644 index 0000000000..015aafbe54 Binary files /dev/null and b/docs/contributing/images/create-gist-menu.png differ diff --git a/docs/contributing/images/create-link.png b/docs/contributing/images/create-link.png new file mode 100644 index 0000000000..1636ec6ae2 Binary files /dev/null and b/docs/contributing/images/create-link.png differ diff --git a/docs/contributing/images/github-pane-toolbar.png b/docs/contributing/images/github-pane-toolbar.png new file mode 100644 index 0000000000..f146ac0b80 Binary files /dev/null and b/docs/contributing/images/github-pane-toolbar.png differ diff --git a/docs/contributing/images/hover-to-add-comment.png b/docs/contributing/images/hover-to-add-comment.png new file mode 100644 index 0000000000..231441e0f9 Binary files /dev/null and b/docs/contributing/images/hover-to-add-comment.png differ diff --git a/docs/contributing/images/manage-connections.png b/docs/contributing/images/manage-connections.png new file mode 100644 index 0000000000..fb8c01d58e Binary files /dev/null and b/docs/contributing/images/manage-connections.png differ diff --git a/docs/contributing/images/open-on-github.png b/docs/contributing/images/open-on-github.png new file mode 100644 index 0000000000..186bda86d9 Binary files /dev/null and b/docs/contributing/images/open-on-github.png differ diff --git a/docs/contributing/images/open-team-explorer.png b/docs/contributing/images/open-team-explorer.png new file mode 100644 index 0000000000..25a750c85f Binary files /dev/null and b/docs/contributing/images/open-team-explorer.png differ diff --git a/docs/contributing/images/pr-create.png b/docs/contributing/images/pr-create.png new file mode 100644 index 0000000000..02aededb85 Binary files /dev/null and b/docs/contributing/images/pr-create.png differ diff --git a/docs/contributing/images/pr-details-checkout-link.png b/docs/contributing/images/pr-details-checkout-link.png new file mode 100644 index 0000000000..543ac38e49 Binary files /dev/null and b/docs/contributing/images/pr-details-checkout-link.png differ diff --git a/docs/contributing/images/pr-details.png b/docs/contributing/images/pr-details.png new file mode 100644 index 0000000000..222b2edc1a Binary files /dev/null and b/docs/contributing/images/pr-details.png differ diff --git a/docs/contributing/images/pr-diff-files.png b/docs/contributing/images/pr-diff-files.png new file mode 100644 index 0000000000..5ae7800009 Binary files /dev/null and b/docs/contributing/images/pr-diff-files.png differ diff --git a/docs/contributing/images/pr-pull-changes.png b/docs/contributing/images/pr-pull-changes.png new file mode 100644 index 0000000000..3073e77536 Binary files /dev/null and b/docs/contributing/images/pr-pull-changes.png differ diff --git a/docs/contributing/images/publish-to-github.png b/docs/contributing/images/publish-to-github.png new file mode 100644 index 0000000000..59c3abb36b Binary files /dev/null and b/docs/contributing/images/publish-to-github.png differ diff --git a/docs/contributing/images/pull-request-list.png b/docs/contributing/images/pull-request-list.png new file mode 100644 index 0000000000..9b5d32b881 Binary files /dev/null and b/docs/contributing/images/pull-request-list.png differ diff --git a/docs/contributing/images/pull-requests-button.png b/docs/contributing/images/pull-requests-button.png new file mode 100644 index 0000000000..365494defb Binary files /dev/null and b/docs/contributing/images/pull-requests-button.png differ diff --git a/docs/contributing/images/pull-requests-button2.png b/docs/contributing/images/pull-requests-button2.png new file mode 100644 index 0000000000..4d03c1b0dc Binary files /dev/null and b/docs/contributing/images/pull-requests-button2.png differ diff --git a/docs/contributing/images/successful-creation-message.png b/docs/contributing/images/successful-creation-message.png new file mode 100644 index 0000000000..45fe34e34b Binary files /dev/null and b/docs/contributing/images/successful-creation-message.png differ diff --git a/docs/contributing/images/team-explorer-sync.png b/docs/contributing/images/team-explorer-sync.png new file mode 100644 index 0000000000..32d4bce4a3 Binary files /dev/null and b/docs/contributing/images/team-explorer-sync.png differ diff --git a/docs/contributing/index.md b/docs/contributing/index.md new file mode 100644 index 0000000000..5bf9eb0791 --- /dev/null +++ b/docs/contributing/index.md @@ -0,0 +1,29 @@ +# Contributing to Projects with GitHub for Visual Studio + +Use GitHub for Visual Studio to manage your projects and work with pull requests. + +### Table of Contents + +- Adding and cloning repositories + - [Publishing an existing project to GitHub](publishing-an-existing-project-to-github.md) + - [Creating an empty repository from Visual Studio](creating-an-empty-repository-from-visual-studio.md) + - [Cloning a repository to Visual Studio](cloning-a-repository-to-visual-studio.md) +- Working with pull requests + - [Viewing pull requests for a repository](viewing-the-pull-requests-for-a-repository.md) + - [Creating a pull request](creating-a-pull-request.md) + - [Reviewing a pull request in Visual Studio](reviewing-a-pull-request-in-visual-studio.md) + - [Making changes to a pull request](making-changes-to-a-pull-request.md) +- [Creating gists](creating-gists.md) +- [Viewing existing code on GitHub](viewing-code-on-github.md) + - [Viewing the selected code on GitHub](viewing-code-on-github.md#viewing-the-selected-code-on-github) + - [Copying the URL of the selected code's location on GitHub](viewing-code-on-github.md#copying-the-url-of-the-selected-codes-location-on-github) + - [Viewing the selected code in blame view on GitHub](viewing-code-on-github.md#viewing-the-selected-code-in-blame-view-on-github) +- [Using the GitHub toolbar](using-the-github-toolbar.md) +- Operations provided by Visual Studio + - [Committing](https://www.visualstudio.com/en-us/docs/git/tutorial/commits) + - [Pushing commits to the remote repository](https://www.visualstudio.com/en-us/docs/git/tutorial/pushing) + - [Fetching and pulling](https://www.visualstudio.com/en-us/docs/git/tutorial/pulling) + - [Working with branches](https://www.visualstudio.com/en-us/docs/git/tutorial/branches) + - [Viewing history](https://www.visualstudio.com/en-us/docs/git/tutorial/history) + - [Ignoring files](https://www.visualstudio.com/en-us/docs/git/tutorial/ignore-files) +- [Contact a human](https://github.com/contact) diff --git a/docs/contributing/making-changes-to-a-pull-request.md b/docs/contributing/making-changes-to-a-pull-request.md new file mode 100644 index 0000000000..d16d728dfb --- /dev/null +++ b/docs/contributing/making-changes-to-a-pull-request.md @@ -0,0 +1,17 @@ +# Making changes to a pull request + +When a topic branch is [checked out](review-a-pull-request-in-visual-studio.md), you can commit changes to it and push and pull like any other branch. If the pull request branch is located in a fork and was checked out from the Pull Request Details view in the GitHub pane, then a remote to that fork will be created automatically and the branch set to track the fork branch. + +## Pulling changes to your local clone + +If a Pull Request is checked out and the author adds new commits to the branch, then the option will be given to pull the changes locally. This works both for pull requests from the same repository and from a fork. + +![Pulling changes by clicking the pull link button](images/pr-pull-changes.png) + +## Pushing changes + +If you make commits locally to a topic branch, then you can push the changes to the remote branch. You can also do this from Git itself or from the Visual Studio Team Explorer **Sync** view. + +> Note: for this to work with Pull Requests that come from forks, then you must be a maintainer on the repository and the Pull Request submitter must have checked [Allow edits from maintainers](https://help.github.com/articles/allowing-changes-to-a-pull-request-branch-created-from-a-fork/) when submitting the Pull Request. + +If there are commits on the branch on the remote repository that you don't have on your local clone, you must pull them to your local clone and [resolve any conflicts](https://help.github.com/articles/addressing-merge-conflicts/) before you can push your local commits back to the remote repository. diff --git a/docs/contributing/publishing-an-existing-project-to-github.md b/docs/contributing/publishing-an-existing-project-to-github.md new file mode 100644 index 0000000000..7b296056ce --- /dev/null +++ b/docs/contributing/publishing-an-existing-project-to-github.md @@ -0,0 +1,14 @@ +# Publishing an existing project to GitHub + +1. Open a solution in Visual Studio. +2. If solution is not already initialized as a Git repository, select **Add to Source Control** from the **File** menu. +![Location of Add to Source Control option in the file menu](images/add-to-source-control.png) +3. Open **Team Explorer**. +![Location of Team Explorer option in the view menu](images/open-team-explorer.png) +4. In Team Explorer, click **Sync**. +![Location of the sync button in the Team Explorer pane](images/team-explorer-sync.png) +5. Click the **Publish to GitHub** button. +![Location of the Publish to GitHub button in the Team Explorer pane](images/publish-to-github.png) +6. Enter a name and description for the repository on GitHub. +7. Check the **Private Repository** box if you want to upload the repository as a private repository on GitHub. You must have a [Developer, Team or Business account](https://github.com/pricing) to create private repositories. +8. Click the **Publish** button. diff --git a/docs/contributing/reviewing-a-pull-request-in-visual-studio.md b/docs/contributing/reviewing-a-pull-request-in-visual-studio.md new file mode 100644 index 0000000000..904af9dab3 --- /dev/null +++ b/docs/contributing/reviewing-a-pull-request-in-visual-studio.md @@ -0,0 +1,51 @@ +# Reviewing a pull request in Visual Studio + +GitHub for Visual Studio provides facilities for reviewing a pull request directly in Visual Studio. + +1. Open a solution in a GitHub repository. + +2. Open **Team Explorer** and click the **Pull Requests** button to open the **GitHub** pane. + + ![Pull Requests button in the Team Explorer pane](images/pull-requests-button.png) + +3. Click the title of the pull request to be reviewed. + +## Viewing a pull request + +The Pull Request Details view shows the current state of the pull request, including information about who created the pull request, the source and target branch, and the files changed. + +![The details of a single pull request in the GitHub pane](images/pr-details.png) + +## Checking out a pull request + +To check out the pull request branch, click the **Checkout [branch]** link where [branch] is the name of the branch that will be checked out. + +![Location of the checkout link in the GitHub pull request details page](images/pr-details-checkout-link.png) + +If the pull request is from a fork then a remote will be added to the forked repository and the branch checked out locally. This remote will automatically be cleaned up when the local branch is deleted. + +> Note that you cannot check out a pull request branch when your working directory has uncommitted changes. First commit or stash your changes and then refresh the Pull Request view. + +## Viewing Changes + +To view the changes in the pull request for a file, double click a file in the **Changed Files** tree. This will open the Visual Studio diff viewer. + +![Diff of two files in the Visual Studio diff viewer](images/pr-diff-files.png) + +You can also right-click on a file in the changed files tree to get more options: + +- **View Changes**: This is the default option that is also triggered when the file is double-clicked. It shows the changes to the file that are introduced by the pull request. +- **View File**: This opens a read-only editor showing the contents of the file in the pull request. +- **View Changes in Solution**: This menu item is only available when the pull request branch is checked out. It shows the changes in the pull request, but the right hand side of the diff is the file in the working directory. This view allows you to use Visual Studio navigation commands such as **Go to Definition (F12)**. +- **Open File in Solution**: This menu item opens the working directory file in an editor. + +## Leaving Comments + +You can add comments to a pull request directly from Visual Studio. When a file is [open in the diff viewer](#viewing-changes) you can click the **Add Comment** icon in the margin to add a comment on a line. + +![Hover over margin to see add comment icon](images/hover-to-add-comment.png) + +Then click the icon on the desired line and leave a comment. +![Add a comment](images/add-comment.png) + +Existing comments left by you or other reviewers will also show up in this margin. Click the icon to open an inline conversation view from which you can review and reply to comments: diff --git a/docs/contributing/using-the-github-toolbar.md b/docs/contributing/using-the-github-toolbar.md new file mode 100644 index 0000000000..0cc353babd --- /dev/null +++ b/docs/contributing/using-the-github-toolbar.md @@ -0,0 +1,29 @@ +# Using the GitHub pane toolbar + +The GitHub pane toolbar provides a way to navigate between views, refresh views, and open the current view on GitHub. + +![The GitHub pane toolbar](images/github-pane-toolbar.png) + +1. Open a solution in a GitHub repository. +2. Open **Team Explorer** and click the **Pull Requests** button to open the **GitHub** pane. + +## Using the back navigation button +1. At the top of the **GitHub** pane, locate the button furthest to the left. This is the **Back** button. +2. Click **Back** to navigate to the previous view. If **Back** is grey, you have reached the initial point in the view navigation. + +## Using the forward navigation button +1. At the top of the **GitHub** pane, locate the second button from the left, which is the **Forward** button. +2. Click **Forward** to navigate to the next view. If **Forward** is grey, you have reached the furthest point in the view navigation. + +## Using the pull request toolbar icon +1. At the top of the **GitHub** pane, locate the third button from the left. This is the **Pull Requests** button. +2. Click **Pull Requests** to navigate to the list of pull requests in the repository. + +## Using the GitHub toolbar button +1. At the top of the **GitHub** pane, locate the fourth button from the left. This is the **View On GitHub** button. +2. While viewing the list of pull requests, click **View On Github**. Your browser will open and navigate to the repository's pull requests on GitHub. +3. While viewing the details of a pull request, click **View On Github**. Your browser will open and navigate to the pull request on GitHub. + +## Using the refresh toolbar button +1. At the top of the **GitHub** pane, locate the fifth button from the left. This is the **Refresh** button. +2. Click **Refresh** to refresh the current view in the **GitHub** pane. diff --git a/docs/contributing/viewing-code-on-github.md b/docs/contributing/viewing-code-on-github.md new file mode 100644 index 0000000000..2f3d587fc3 --- /dev/null +++ b/docs/contributing/viewing-code-on-github.md @@ -0,0 +1,21 @@ +# Viewing existing code on GitHub + +GitHub for Visual Studio enables easy navigation to code that exists on GitHub directly from the Visual Studio code editor. + +1. Open a solution in a GitHub repository. +2. Open *Solution Explorer* by clicking on its tab, or via the *View* menu. +3. In *Solution Explorer*, double click on a file to open it in Visual Studio code editor. +3. In the code editor, highlight the section of text that you want to view in the browser. + +## Viewing the selected code on GitHub +1. Right click and select **Open on GitHub** from the **GitHub** submenu. +![Open on GitHub selection in the GitHub context submenu](images/open-on-github.png) +2. Your browser will open and navigate to the code on GitHub. + +## Copying the URL of the selected code's location on GitHub +1. Right click and select **Copy link to clipboard** from the **GitHub** submenu. The URL will be copied to the clipboard. +2. Paste the URL into your browser to view it on GitHub. + +## Viewing the selected code in blame view on GitHub +1. Right click and select **Blame** from the **GitHub** submenu. +2. Your browser will open and navigate to the code in blame view on GitHub. diff --git a/docs/contributing/viewing-the-pull-requests-for-a-repository.md b/docs/contributing/viewing-the-pull-requests-for-a-repository.md new file mode 100644 index 0000000000..83a2ae3e7c --- /dev/null +++ b/docs/contributing/viewing-the-pull-requests-for-a-repository.md @@ -0,0 +1,15 @@ +# Viewing the pull requests for a repository + +GitHub for Visual Studio exposes the pull requests for the current repository and lets you create new pull requests and review pull requests from other contributors. + +1. [Sign in](../getting-started/authenticating-to-github.md) to GitHub. +2. Open a solution in a GitHub repository. +3. Open **Team Explorer** and click the **Pull Requests** button to open the **GitHub** pane. +![Pull Requests button in the Team Explorer pane](images/pull-requests-button2.png) +4. The open pull requests will be displayed. +![Pull requests in the GitHub pane](images/pull-request-list.png) +5. Change the Open/Closed filter by clicking the **Open** link and selecting the filter you want to use from the dropdown. +6. Filter pull requests by Assignee by clicking the **Assignee** link and selecting the assignee you want to view from the dropdown. +7. Filter pull requests by Author by clicking the **Author** link and selecting the author you want to view from the dropdown. +8. Click on a pull request title to [view the pull request details and review the pull request](review-a-pull-request-in-visual-studio.md) +9. Click on the **Create New** link to [create a pull request from the current branch](sending-a-pull-request.md) diff --git a/docs/developer/dialog-views-with-connections.md b/docs/developer/dialog-views-with-connections.md new file mode 100644 index 0000000000..856b4ae67e --- /dev/null +++ b/docs/developer/dialog-views-with-connections.md @@ -0,0 +1,24 @@ +# Dialog Views with Connections + +Some dialog views need a connection to operate - if there is no connection, a login dialog should be shown: for example, clicking Create Gist without a connection will first prompt the user to log in. + +Achieving this is simple, first make your view model interface implement `IConnectionInitializedViewModel` and do any initialization that requires a connection in the `InitializeAsync` method in your view model: + +```csharp +public Task InitializeAsync(IConnection connection) +{ + // .. at this point, you're guaranteed to have a connection. +} +``` + +To show the dialog, call `IShowDialogService.ShowWithFirstConnection` instead of `Show`: + +```csharp +public async Task ShowExampleDialog() +{ + var viewModel = serviceProvider.ExportProvider.GetExportedValue(); + await showDialog.ShowWithFirstConnection(viewModel); +} +``` + +`ShowFirstConnection` first checks if there are any logged in connections. If there are, the first logged in connection will be passed to `InitializeAsync` and the view shown immediately. If there are no logged in connections, the login view will first be shown. Once the user has successfully logged in, the new connection will be passed to `InitalizeAsync` and the view shown. \ No newline at end of file diff --git a/docs/developer/how-viewmodels-are-turned-into-views.md b/docs/developer/how-viewmodels-are-turned-into-views.md new file mode 100644 index 0000000000..a0df01241a --- /dev/null +++ b/docs/developer/how-viewmodels-are-turned-into-views.md @@ -0,0 +1,86 @@ +# How ViewModels are Turned into Views + +We make use of the [MVVM pattern](https://msdn.microsoft.com/en-us/library/ff798384.aspx), in which application level code is not aware of the view level. MVVM takes advantage of the fact that `DataTemplate`s can be used to create views from view models. + +## DataTemplates + +[`DataTemplate`](https://docs.microsoft.com/en-us/dotnet/framework/wpf/data/data-templating-overview)s are a WPF feature that allow you to define the presentation of your data. Consider a simple view model: + +```csharp +public class ViewModel +{ + public string Greeting => "Hello World!"; +} +``` + +And a window: + +```csharp +public class MainWindow : Window +{ + public MainWindow() + { + DataContext = new ViewModel(); + InitializeComponent(); + } +} +``` + +```xml + + + +``` + +Here we're binding the `Content` of the `Window` to the `Window.DataContext`, which we're setting in the constructor to be an instance of `ViewModel`. + +One can choose to display the `ViewModel` instance in any way we want by using a `DataTemplate`: + +```xml + + + + + + + + + + + + +``` + +This is the basis for converting view models to views. + +## ViewLocator + +There are currently two top-level controls for our UI: + +- [GitHubDialogWindow](../../src/GitHub.VisualStudio/Views/Dialog/GitHubDialogWindow.xaml) for the dialog which shows the login, clone, etc views +- [GitHubPaneView](../../src/GitHub.VisualStudio/Views/GitHubPane/GitHubPaneView.xaml) for the GitHub pane + +In the resources for each of these top-level controls we define a `DataTemplate` like so: + +```xml + + + + +``` + +The `DataTemplate.DataType` here applies the template to all classes inherited from [`GitHub.ViewModels.ViewModelBase`](../../src/GitHub.Exports.Reactive/ViewModels/ViewModelBase.cs) [1]. The template defines a single `ContentControl` whose contents are created by a `ViewLocator`. + +The [`ViewLocator`](../../src/GitHub.VisualStudio/Views/ViewLocator.cs) class is an `IValueConverter` which then creates an instance of the appropriate view for the view model using MEF. + +And thus a view model becomes a view. + +[1]: it would be nice to make it apply to all classes that inherit `IViewModel` but unfortunately WPF's `DataTemplate`s don't work with interfaces. diff --git a/docs/developer/implementing-a-dialog-view.md b/docs/developer/implementing-a-dialog-view.md new file mode 100644 index 0000000000..332ddd4aa9 --- /dev/null +++ b/docs/developer/implementing-a-dialog-view.md @@ -0,0 +1,113 @@ +# Implementing a Dialog View + +GitHub for Visual Studio has a common dialog which is used to show the login, clone, create repository etc. operations. To add a new view to the dialog and show the dialog with this view, you need to do the following: + +## Create a View Model and Interface + +- Create an interface for the view model that implements `IDialogContentViewModel` in `GitHub.Exports.Reactive\ViewModels\Dialog` +- Create a view model that inherits from `NewViewModelBase` and implements the interface in `GitHub.App\ViewModels\Dialog` +- Export the view model with the interface as the contract and add a `[PartCreationPolicy(CreationPolicy.NonShared)]` attribute + +A minimal example that just exposes a command that will dismiss the dialog: + +```csharp +using System; +using ReactiveUI; + +namespace GitHub.ViewModels.Dialog +{ + public interface IExampleDialogViewModel : IDialogContentViewModel + { + ReactiveCommand Dismiss { get; } + } +} +``` + +```csharp +using System; +using System.ComponentModel.Composition; +using ReactiveUI; + +namespace GitHub.ViewModels.Dialog +{ + [Export(typeof(IExampleDialogViewModel))] + [PartCreationPolicy(CreationPolicy.NonShared)] + public class ExampleDialogViewModel : ViewModelBase, IExampleDialogViewModel + { + [ImportingConstructor] + public ExampleDialogViewModel() + { + Dismiss = ReactiveCommand.Create(); + } + + public string Title => "Example Dialog"; + + public ReactiveCommand Dismiss { get; } + + public IObservable Done => Dismiss; + } +} +``` + +## Create a View + +- Create a WPF `UserControl` under `GitHub.VisualStudio\Views\Dialog` +- Add an `ExportViewFor` attribute with the type of the view model interface +- Add a `PartCreationPolicy(CreationPolicy.NonShared)]` attribute + +Continuing the example above: + +```xml + + + +``` + +```csharp +using System.ComponentModel.Composition; +using System.Windows.Controls; +using GitHub.Exports; +using GitHub.ViewModels.Dialog; + +namespace GitHub.VisualStudio.Views.Dialog +{ + [ExportViewFor(typeof(IExampleDialogViewModel))] + [PartCreationPolicy(CreationPolicy.NonShared)] + public partial class ExampleDialogView : UserControl + { + public ExampleDialogView() + { + InitializeComponent(); + } + } +} +``` + +## Show the Dialog! + +To show the dialog you will need an instance of the `IShowDialogService` service. Once you have that, simply call the `Show` method with an instance of your view model. + +```csharp +var viewModel = new ExampleDialogViewModel(); +showDialog.Show(viewModel) +``` + +## Optional: Add a method to `DialogService` + +Creating a view model like this may be the right thing to do, but it's not very reusable or testable. If you want your dialog to be easy reusable, add a method to `DialogService`: + +```csharp +public async Task ShowExampleDialog() +{ + var viewModel = factory.CreateViewModel(); + await showDialog.Show(viewModel); +} +``` + +Obviously, add this method to `IDialogService` too. + +Note that these methods are `async` - this means that if you need to do asynchronous initialization of your view model, you can do it here before calling `showDialog`. \ No newline at end of file diff --git a/docs/developer/implementing-github-pane-page.md b/docs/developer/implementing-github-pane-page.md new file mode 100644 index 0000000000..bd17b65af6 --- /dev/null +++ b/docs/developer/implementing-github-pane-page.md @@ -0,0 +1,122 @@ +# Implementing a GitHub Pane Page + +The GitHub pane displays GitHub-specific functionality in a dockable pane. To add a new page to the GitHub pane, you need to do the following: + +## Create a View Model and Interface + +- Create an interface for the view model that implements `IPanePageViewModel` in `GitHub.Exports.Reactive\ViewModels\GitHubPane` +- Create a view model that inherits from `PanePageViewModelBase` and implements the interface in `GitHub.App\ViewModels\GitHubPane` +- Export the view model with the interface as the contract and add a `[PartCreationPolicy(CreationPolicy.NonShared)]` attribute + +A minimal example that just exposes a command that will navigate to the pull request list: + +```csharp +using System; +using ReactiveUI; + +namespace GitHub.ViewModels.GitHubPane +{ + public interface IExamplePaneViewModel : IPanePageViewModel + { + ReactiveCommand GoToPullRequests { get; } + } +} +``` + +```csharp +using System; +using System.ComponentModel.Composition; +using ReactiveUI; + +namespace GitHub.ViewModels.GitHubPane +{ + [Export(typeof(IExamplePaneViewModel))] + [PartCreationPolicy(CreationPolicy.NonShared)] + public class ExamplePaneViewModel : PanePageViewModelBase, IExamplePaneViewModel + { + [ImportingConstructor] + public ExamplePaneViewModel() + { + GoToPullRequests = ReactiveCommand.Create(); + GoToPullRequests.Subscribe(_ => NavigateTo("/pulls")); + } + + public ReactiveCommand GoToPullRequests { get; } + } +} +``` + +## Create a View + +- Create a WPF `UserControl` under `GitHub.VisualStudio\ViewsGitHubPane` +- Add an `ExportViewFor` attribute with the type of the view model interface +- Add a `PartCreationPolicy(CreationPolicy.NonShared)]` attribute + +Continuing the example above: + +```xml + + + + +``` + +```csharp +using System.ComponentModel.Composition; +using System.Windows.Controls; +using GitHub.Exports; +using GitHub.ViewModels.Dialog; + +namespace GitHub.VisualStudio.Views.Dialog +{ + [ExportViewFor(typeof(IExampleDialogViewModel))] + [PartCreationPolicy(CreationPolicy.NonShared)] + public partial class ExampleDialogView : UserControl + { + public ExampleDialogView() + { + InitializeComponent(); + } + } +} +``` + +## Add a Route to GitHubPaneViewModel + +Now you need to add a route to the `GitHubPaneViewModel`. To add a route, you must do two things: + +- Add a method to `GitHubPaneViewModel` +- Add a URL handler to `GitHubPaneViewModel.NavigateTo` + +So lets add the `ShowExample` method to `GitHubPaneViewModel`: + +```csharp +public Task ShowExample() +{ + return NavigateTo(x => Task.CompletedTask); +} +``` +Here we call `NavigateTo` with the type of the interface of our view model. We're passing a lambda that simply returns `Task.CompletedTask` as the parameter: usually here you'd call an async initialization method on the view model, but since we don't have one in our simple example we just return a completed task. + +Next we add a URL handler: our URL is going to be `github://pane/example` so we need to add a route that checks that the URL's `AbsolutePath` is `/example` and if so call the method we added above. This code is added to `GitHubPaneViewModel.NavigateTo`: + +```csharp +else if (uri.AbsolutePath == "/example") +{ + await ShowExample(); +} +``` + +For the sake of the example, we're going to show our new page as soon as the GitHub Pane is shown and the user is logged-in with an open repository. To do this, simply change `GitHubPaneViewModel.ShowDefaultPage` to the following: + +```csharp +public Task ShowDefaultPage() => ShowExample(); +``` + +When you run the extension and show the GitHub pane, our new example page should be shown. Clicking on the button in the page will navigate to the pull request list. \ No newline at end of file diff --git a/docs/developer/multi-paged-dialogs.md b/docs/developer/multi-paged-dialogs.md new file mode 100644 index 0000000000..1e014f516c --- /dev/null +++ b/docs/developer/multi-paged-dialogs.md @@ -0,0 +1,77 @@ +# Multi-paged Dialogs + +Some dialogs will be multi-paged - for example the login dialog has a credentials page and a 2Fa page that is shown if two-factor authorization is required. + +## The View Model + +To help implement view models for a multi-page dialog there is a useful base class called `PagedDialogViewModelBase`. The typical way of implementing this is as follows: + +- Define each page of the dialog as you would [implement a single dialog view model](implementing-a-dialog-view.md) +- Implement a "container" view model for the dialog that inherits from `PagedDialogViewModel` +- Import each page into the container view model +- Add logic to switch between pages by setting the `PagedDialogViewModelBase.Content` property +- Add a `Done` observable + +Here's a simple example of a container dialog that has two pages. The pages are switched using `ReactiveCommand`s: + +```csharp +using System; +using System.ComponentModel.Composition; + +namespace GitHub.ViewModels.Dialog +{ + [Export(typeof(IExamplePagedDialogViewModel))] + [PartCreationPolicy(CreationPolicy.NonShared)] + public class ExamplePagedDialogViewModel : PagedDialogViewModelBase, + IExamplePagedDialogViewModel + { + [ImportingConstructor] + public ExamplePagedDialogViewModel( + IPage1ViewModel page1, + IPage2ViewModel page2) + { + Content = page1; + page1.Next.Subscribe(_ => Content = page2); + page2.Previous.Subscribe(_ => Content = page1); + Done = Observable.Merge(page2.Done, page2.Done); + } + + public override IObservable Done { get; } + } +} +``` + +## The View + +The view in this case is very simple: it just needs to display the `Content` property of the container view model: + +```xml + + +``` + +```csharp +using System; +using System.ComponentModel.Composition; +using System.Windows.Controls; +using GitHub.Exports; +using GitHub.ViewModels.Dialog; + +namespace GitHub.VisualStudio.Views.Dialog +{ + [ExportViewFor(typeof(IExamplePagedDialogViewModel))] + [PartCreationPolicy(CreationPolicy.NonShared)] + public partial class ExamplePagedDialogView : UserControl + { + public NewLoginView() + { + InitializeComponent(); + } + } +} +``` + +> Note: this is such a common pattern, you don't actually need to define your own view! Simply add the `[ExportViewFor(...)]` attribute to the existing `ContentView` class. \ No newline at end of file diff --git a/docs/developer/readme.md b/docs/developer/readme.md new file mode 100644 index 0000000000..b0c06b2e81 --- /dev/null +++ b/docs/developer/readme.md @@ -0,0 +1,10 @@ +# Developer Documentation + +Documentation for hacking on GitHub for Visual Studio: + +- User Interface + - [How ViewModels are Turned into Views](how-viewmodels-are-turned-into-views.md) + - [Implementing a Dialog View](implementing-a-dialog-view.md) + - [Dialog Views with Connections](dialog-views-with-connections.md) + - [Multi-Paged Dialogs](multi-paged-dialogs.md) + diff --git a/docs/getting-started/authenticating-to-github.md b/docs/getting-started/authenticating-to-github.md new file mode 100644 index 0000000000..d3f83ba438 --- /dev/null +++ b/docs/getting-started/authenticating-to-github.md @@ -0,0 +1,50 @@ +# Authenticating to GitHub + +## How to login to GitHub or GitHub Enterprise + +1. In Visual Studio, select **Team Explorer** from the **View** menu. +
Team Explorer in the view menu
+1. In the Team Explorer pane, click the **Manage Connectios** toolbar icon. +
Manage connections toolbar icon in the Team Explorer pane
+1. Click the **Connect** link in the GitHub section. +
Connect to GitHub
+ + If you're connected to a TFS instance, click on the **Sign in** link instead +
Sign in to GitHub
+ + If none of these options are visible, click **Manage Connections** and then **Connect to GitHub**. +
Connect to GitHub in the manage connections dropdown in the Team Explorer pane
+1. In the **Connect to GitHub dialog** choose **GitHub** or **GitHub Enterprise**, depending on which product you're using. + +**GitHub option**: +
Connect to GitHub dialog view
+ +- To sign in with credentials, enter either username or email and password. +- To sign in with SSO, select `Sign in with your browser`. + +**GitHub Enterprise option**: +
Connect to GitHub Enterprise dialog view
+ +- To sign in with SSO, enter the GitHub Enterprise server address and select `Sign in with your browser`. +- To sign in with credentials, enter the GitHub Enterprise server address. + - If a `Password` field appears, enter your password. + - If a `Token` field appears, enter a valid token. You can create personal access tokens by [following the instructions in the section below](#personal_access_tokens). + +Before you authenticate, you must already have a GitHub or GitHub Enterprise account. + +- For more information on creating a GitHub account, see "[Signing up for a new GitHub account](https://help.github.com/articles/signing-up-for-a-new-github-account/)". +- For a GitHub Enterprise account, contact your GitHub Enterprise site administrator. + +### Personal access tokens + +If all signin options above fail, you can manually create a personal access token and use it as your password. + +The scopes for the personal access token are: `user`, `repo`, `gist`, and `write:public_key`. +- *user* scope: Grants access to the user profile data. We currently use this to display your avatar and check whether your plans lets you publish private repositories. +- *repo* scope: Grants read/write access to code, commit statuses, invitations, collaborators, adding team memberships, and deployment statuses for public and private repositories and organizations. This is needed for all git network operations (push, pull, fetch), and for getting information about the repository you're currently working on. +- *gist* scope: Grants write access to gists. We use this in our gist feature, so you can highlight code and create gists directly from Visual Studio +- *write:public_key* scope: Grants access to creating, listing, and viewing details for public keys. This will allows us to add ssh support to your repositories, if you are unable to go through https (this feature is not available yet, this scope is optional) + +For more information on creating personal access tokens, see "[Creating a personal access token for the command line](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line). + +For more information on authenticating with SAML single sign-on, see "[About authentication with SAML single sign-on](https://help.github.com/articles/about-authentication-with-saml-single-sign-on)." diff --git a/docs/getting-started/configuring-git-in-visual-studio.md b/docs/getting-started/configuring-git-in-visual-studio.md new file mode 100644 index 0000000000..1c122ba3c4 --- /dev/null +++ b/docs/getting-started/configuring-git-in-visual-studio.md @@ -0,0 +1,17 @@ +# Configuring Git in Visual Studio + +# Setting your name and email + +The name and email that will be displayed with your commits can be set from Team Explorer's settings. + +1. Open **Team Explorer** by clicking on its tab next to *Solution Explorer*, or via the *View* menu. + +2. Click the **Settings** button in Team Explorer. + + ![The settings button in Team Explorer pane](images/settings-button.png) + +3. Click **Global Settings** in the Team Explorer Settings page + + ![The global settings button in the Team Explorer settings page](images/global-settings-link.png) + +4. Enter the name and email that you would like to appear in commits. diff --git a/docs/getting-started/images/connect-to-github-dialog.png b/docs/getting-started/images/connect-to-github-dialog.png new file mode 100644 index 0000000000..c7ea91774a Binary files /dev/null and b/docs/getting-started/images/connect-to-github-dialog.png differ diff --git a/docs/getting-started/images/connect-to-github-enterprise-dialog.png b/docs/getting-started/images/connect-to-github-enterprise-dialog.png new file mode 100644 index 0000000000..4957025721 Binary files /dev/null and b/docs/getting-started/images/connect-to-github-enterprise-dialog.png differ diff --git a/docs/getting-started/images/connect_to_github.png b/docs/getting-started/images/connect_to_github.png new file mode 100644 index 0000000000..49cf9a707d Binary files /dev/null and b/docs/getting-started/images/connect_to_github.png differ diff --git a/docs/getting-started/images/global-settings-link.png b/docs/getting-started/images/global-settings-link.png new file mode 100644 index 0000000000..88ca43b6ea Binary files /dev/null and b/docs/getting-started/images/global-settings-link.png differ diff --git a/docs/getting-started/images/install-from-gallery.png b/docs/getting-started/images/install-from-gallery.png new file mode 100644 index 0000000000..5731d67192 Binary files /dev/null and b/docs/getting-started/images/install-from-gallery.png differ diff --git a/docs/getting-started/images/manage_connections.png b/docs/getting-started/images/manage_connections.png new file mode 100644 index 0000000000..a3f5a7c71a Binary files /dev/null and b/docs/getting-started/images/manage_connections.png differ diff --git a/docs/getting-started/images/settings-button.png b/docs/getting-started/images/settings-button.png new file mode 100644 index 0000000000..4e7b577faf Binary files /dev/null and b/docs/getting-started/images/settings-button.png differ diff --git a/docs/getting-started/images/sign-in-to-github-provider.png b/docs/getting-started/images/sign-in-to-github-provider.png new file mode 100644 index 0000000000..e4a9b1748e Binary files /dev/null and b/docs/getting-started/images/sign-in-to-github-provider.png differ diff --git a/docs/getting-started/images/sign-in-to-github.png b/docs/getting-started/images/sign-in-to-github.png new file mode 100644 index 0000000000..1ea95fb069 Binary files /dev/null and b/docs/getting-started/images/sign-in-to-github.png differ diff --git a/docs/getting-started/images/update-from-gallery.png b/docs/getting-started/images/update-from-gallery.png new file mode 100644 index 0000000000..f7cdb7c954 Binary files /dev/null and b/docs/getting-started/images/update-from-gallery.png differ diff --git a/docs/getting-started/images/view_team_explorer.png b/docs/getting-started/images/view_team_explorer.png new file mode 100644 index 0000000000..364934e038 Binary files /dev/null and b/docs/getting-started/images/view_team_explorer.png differ diff --git a/docs/getting-started/images/vs2015-installer.png b/docs/getting-started/images/vs2015-installer.png new file mode 100644 index 0000000000..5f442547ec Binary files /dev/null and b/docs/getting-started/images/vs2015-installer.png differ diff --git a/docs/getting-started/images/vs2017-installer.png b/docs/getting-started/images/vs2017-installer.png new file mode 100644 index 0000000000..b5c730fff2 Binary files /dev/null and b/docs/getting-started/images/vs2017-installer.png differ diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md new file mode 100644 index 0000000000..dc0575d1a9 --- /dev/null +++ b/docs/getting-started/index.md @@ -0,0 +1,13 @@ +# Getting Started with GitHub for Visual Studio + +## [Installing GitHub for Visual Studio](installing-github-for-visual-studio.md) + +GitHub for Visual Studio is available for Visual Studio 2015 and later. The Community, Professional and Enterprise editions are supported. + +## [Authenticating to GitHub](authenticating-to-github.md) + +Add your GitHub.com or GitHub Enterprise account information to Visual Studio so you can access your repositories. + +### [Configuring Git in Visual Studio](configuring-git-in-visual-studio.md) + +Configure how Visual Studio interacts with GitHub \ No newline at end of file diff --git a/docs/getting-started/installing-github-for-visual-studio.md b/docs/getting-started/installing-github-for-visual-studio.md new file mode 100644 index 0000000000..11decbfb7e --- /dev/null +++ b/docs/getting-started/installing-github-for-visual-studio.md @@ -0,0 +1,71 @@ +# Installing the GitHub Extension for Visual Studio + +GitHub for Visual Studio is an extension for Microsoft Visual Studio 2015 and later. It is not supported on Visual Studio Code, Visual Studio Express or Visual Studio for Mac. + +## Installing for all versions of Visual Studio 2015 and higher + +If you already have Visual Studio 2015 or higher installed, you can install the extension into all your versions of Visual Studio with the following steps: + +1. Visit the [GitHub for Visual Studio](https://visualstudio.github.com/) site. +2. Click the **Download GitHub Extension for Visual Studio** button. +3. In your computer's **Downloads** folder, double-click **GitHub.VisualStudio.vsix**. +4. In the pop-up window, click **Install**. +5. After the installation is completed, run Visual Studio. + +## Installing from the Visual Studio gallery + +If you're currently running Visual Studio 2015 or higher, you can install the extension from the Visual Studio gallery. + +1. In Visual Studio, open the **Tools** menu and click **Extensions and Updates** + +2. On the left side of the **Extensions and Updates** dialog, select **Online - Visual Studio gallery** + +3. In the search box on the top right side, type **GitHub** + +4. Select the **GitHub Extension for Visual Studio** entry and click **Download** + + ![Installing GitHub for Visual Studio in the settings extensions and updates gallery](images/install-from-gallery.png) + +5. After installation is completed, restart Visual Studio. + +## If you do not have Visual Studio installed yet + +When you install Visual Studio, you can include the GitHub Extension for Visual Studio for installation, as it is available in the Visual Studio installer. + +### Visual Studio 2015 + +**Note:** The Visual Studio 2015 installer is not guaranteed to install the latest version of the extension. Once the Visual Studio installation is complete, [update the extension](#updating-the-extension) from the Visual Studio gallery, or [run the installer](#installing-for-all-versions-of-visual-studio-2015-and-higher) from our website. + +1. Start the Visual Studio 2015 installer. +2. Scroll down to **Common Tools** and check **GitHub Extension for Visual Studio**. + ![GitHub Extension for Visual Studio in the common tools section of the Visual Studio 2015 installer](images/vs2015-installer.png) +3. Click the **Install** button. +4. Once installation is complete, run Visual Studio 2015 and [update the extension](#updating-the-extension) + +### Visual Studio 2017 + +**Note:** The Visual Studio 2017 installer is not guaranteed to install the latest version of the extension. Once the Visual Studio installation is complete, [update the extension](#updating-the-extension) from the Visual Studio gallery, or [run the installer](#installing-for-all-versions-of-visual-studio-2015-and-higher) from our website. + +1. Start the Visual Studio 2017 installer. + +2. Select the **Individual components** tab at the top. + +3. Scroll down to **Code tools** and check **GitHub Extension for Visual Studio**. + + ![GitHub Extension for Visual Studio in the code tools section of the Visual Studio 2017 installer](images/vs2017-installer.png) + +4. Click the **Modify** button. + +5. Once installation is complete, run Visual Studio 2017 and [update the extension](#updating-the-extension) + +## Updating the extension + +**Note:** If you're currently running Visual Studio 2015 or higher and the extension is already installed, Visual Studio will check for and install updates automatically every 24 hours. For this update process to run, Visual Studio must not be running. + +Visual Studio 2017 will not run automatic updates until you update the extension at least once. + +1. In Visual Studio, open the **Tools** menu and click **Extensions and Updates** +2. On the left side of the **Extensions and Updates** dialog, select **Updates - Visual Studio gallery** +3. If there are updates available, an entry titled **GitHub Extension for Visual Studio** will appear on the list. Select it and click **Update** + ![Updating GitHub for Visual Studio in the settings extensions and updates gallery](images/update-from-gallery.png) +4. After installation is completed, restart Visual Studio. diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 0000000000..6916ea3c3c --- /dev/null +++ b/docs/readme.md @@ -0,0 +1,9 @@ +# GitHub for Visual Studio Documentation + +### [Getting Started with GitHub for Visual Studio](getting-started/index.md) + +Get GitHub for Visual Studio set up to bring the GitHub flow to Visual Studio. Authenticate to GitHub.com or GitHub Enterprise, keep the extension up-to-date, and review your preferred settings. + +### [Contributing to Projects with GitHub for Visual Studio](contributing/index.md) + +Use GitHub for Visual Studio to manage your projects and work with pull requests. \ No newline at end of file diff --git a/documentation/manifest.md b/documentation/manifest.md deleted file mode 100644 index 3a68a88220..0000000000 --- a/documentation/manifest.md +++ /dev/null @@ -1,112 +0,0 @@ -# First Launch - - **If last solution was in a git repo hosted on GitHub** - - **Team Explorer Home page shows:** - - [ ] GitHub header, repo information - - [ ] Pull Requests button - - [ ] Pulse button - - [ ] Graphs button - - [ ] Issues button - - **If last solution was in a git repo not hosted on GitHub** - - [ ] Team Explorer Home page does not show any github information - - **Go to Team Explorer Connect page** - - [ ] GitHub invitation section in Hosted Service Providers area is visible with Connect... and Sign up links - - [ ] **Click on Connect** - - [ ] Connect to GitHub dialog appears - - [ ] GitHub option underlined - - [ ] Cursor on username field - - [ ] Login button disabled - - [ ] Link to sign up at the bottom - - [ ] **Fill out login information** - - [ ] **On successful login** - - [ ] Connect dialog disappears - - [ ] GitHub invitation section in Connect page disappears - - [ ] GitHub connection appears above Hosted Service Providers area with Clone, Create and Sign out action links. As long as it's above Local Git Repositories, it's good - -# In Team Explorer Connect page (logged in) - - [ ] **Click on Clone action link** - - [ ] Clone dialog appears - - [ ] List of user repositories is populated - - [ ] Path field contains default cloning path C:\Users\[user]\Source\Repos - - [ ] Cursor is in Search Repositories field - - [ ] Clone button is disabled - - [ ] Typing in the Search Repositories field filters the list - - [ ] Clicking on the browse action link opens a file explorer - - [ ] Selecting a directory in the file explorer changes the contents of the Path field to the new path - - [ ] Selecting a repository from the list enables the clone button - - [ ] ctrl-clicking a selection in the list removes the selection and disables the clone button - - [ ] Hovering over the clone button (when enabled) animates the button (reversing colors) - - [ ] **Select a repository and click Clone** - - [ ] Clone dialog disappears - - [ ] Progress bar appears in the Team Explorer Connect page with cloning progress (depending on repo size) - - [ ] Notification appears in Team Explorer Connect page: "The repository was cloned successfully. username/repo-name has been successfully created. Create a new project or solution." with proper links displayed. - - [ ] Repository shows up in the "Local Git Repositories" list - - [ ] **Double-click the cloned repository in the "Local Git Repositories" list** - - [ ] Team Explorer view changes to Home page - - [ ] GitHub header and repo information is shown - - [ ] Click Clone in "Local Git Repositories" List and copy/paste a repo from .com. Clone and verify the message displays, "The repository was cloned successfully." - - [ ] **Click on Create action link** - - [ ] Create dialog appears - - [ ] Cursor is on the Name field - - [ ] Create button is disabled - - [ ] Local path is set to default cloning path C:\Users\[user]\Source\Repos - - [ ] Git ignore is set to VisualStudio - - [ ] User is set to current logged user - - [ ] Tabbing through the fields follows visual placement of fields - - [ ] Filling the name field enables the Create button - - [ ] Hovering over the Create button animates it (reversing colors) - - [ ] Clicking on the browse action link opens a file explorer - - [ ] Selecting a directory in the file explorer changes the contents of the Path field to the new path - - [ ] **Fill out the name field and click Create** - - [ ] Dialog disappears - - [ ] Notification appears in Team Explorer Connect page: "The repository was created successfully" - - [ ] Repository shows up in the "Local Git Repositories" list - - [ ] **Double-click the created repository in the "Local Git Repositories" list** - - [ ] Team Explorer view changes to Home page - - [ ] GitHub header and repo information is shown -- [ ] **Publishing a local repo** - - [ ] File - New - Project - Console Application (or any type of project, doesn't matter much) - - [ ] Select "Add to source control" from the dialog and click Ok - - [ ] Select "Git" from the Choose Source Control dialog - - [ ] Verify that Team Explorer home page does *not* have a GitHub section -- [ ] **Click "Sync"** - - [ ] Synchronization page opens with "Publish to GitHub" section -- [ ] **Click "Get Started" in the "Publish to GitHub" section** - - [ ] Contents of section change to a publish form with: - - [ ] User dropdown - - [ ] Pre-filled name field with project name - - [ ] Description field - - [ ] Private checkbox - - [ ] Publish button -- [ ] **Publish button is enabled and private checkbox is unchecked (and disabled if user cannot create private repos)** - - [ ] Click on "Publish" - - [ ] Form becomes disabled - - [ ] Progress bar appears above Synchronization title - - [ ] Team Explorer view changes to Home page - - [ ] Notification appears: "Repository published successfully" - - [ ] Publish a private repo and verify on .com that it's private -- [ ] **Project section (Home button)*** - - [ ] Click on "Home" icon - - [ ] Verify Project has the following sections/buttons when signed into GitHub.com and the Repository is enabled: Pull Requests, Pulse, Graphs, Issues, Wikis - - [ ] Verify Pulse button navigates to Pulse page on Github.com - - [ ] Verify Graphs button navigates to Graphs page on GitHub.com - - [ ] Verify Wikis button navigates to Wikis page on GitHub.com (when logged in and the repository is enabled) - - [ ] Verify Issues button navigates to Issues page on GitHub.com (when logged in and the repository is enabled) - - -# Connect page when logged in to TFS - - [ ] **Connect to a TFS project** - - [ ] Login to GitHub - - [ ] Team Explorer Connect page: GitHub section appears above TFS section with Clone | Create | Sign out links - - [ ] Log out of GitHub - - [ ] Team Explorer Connect page: GitHub section appears above TFS section with Clone | Create | Login links - - [ ] Disconnect from TFS (right click on project and "Remove" - - [ ] Team Explorer Connect page: GitHub invitation section appears in Hosted Service Providers with Connect.. and Sign up links - -# Connections -- [ ] **Login to GitHub.com, then click on the "Manage Connections" header and "Connect to GitHub"** - - [ ] Login dialog appears - - [ ] GitHub Enterprise is underlined - - [ ] Form has 3 fields - username, password and address - - [ ] Login button is disabled -- [ ] **Login to an enterprise instance** - - [ ] Team Explorer Connect page shows two github connections - one titled GitHub, another with the enterprise url diff --git a/install.cmd b/install.cmd new file mode 100644 index 0000000000..0f46241066 --- /dev/null +++ b/install.cmd @@ -0,0 +1,3 @@ +@if "%1" == "" echo Please specify Debug or Release && EXIT /B +tools\VsixUtil\vsixutil /install "build\%1\GitHub.VisualStudio.vsix" +@echo Installed %1 build of GitHub for Visual Studio diff --git a/lib/.gitignore b/lib/.gitignore new file mode 100644 index 0000000000..296234c026 --- /dev/null +++ b/lib/.gitignore @@ -0,0 +1 @@ +!*.nupkg diff --git a/lib/Microsoft.TeamFoundation.Client.dll b/lib/14.0/Microsoft.TeamFoundation.Client.dll similarity index 100% rename from lib/Microsoft.TeamFoundation.Client.dll rename to lib/14.0/Microsoft.TeamFoundation.Client.dll diff --git a/lib/Microsoft.TeamFoundation.Common.dll b/lib/14.0/Microsoft.TeamFoundation.Common.dll similarity index 100% rename from lib/Microsoft.TeamFoundation.Common.dll rename to lib/14.0/Microsoft.TeamFoundation.Common.dll diff --git a/lib/Microsoft.TeamFoundation.Controls.dll b/lib/14.0/Microsoft.TeamFoundation.Controls.dll similarity index 100% rename from lib/Microsoft.TeamFoundation.Controls.dll rename to lib/14.0/Microsoft.TeamFoundation.Controls.dll diff --git a/lib/Microsoft.TeamFoundation.Git.Client.dll b/lib/14.0/Microsoft.TeamFoundation.Git.Client.dll similarity index 100% rename from lib/Microsoft.TeamFoundation.Git.Client.dll rename to lib/14.0/Microsoft.TeamFoundation.Git.Client.dll diff --git a/lib/Microsoft.TeamFoundation.Git.Controls.dll b/lib/14.0/Microsoft.TeamFoundation.Git.Controls.dll similarity index 100% rename from lib/Microsoft.TeamFoundation.Git.Controls.dll rename to lib/14.0/Microsoft.TeamFoundation.Git.Controls.dll diff --git a/lib/Microsoft.TeamFoundation.Git.Provider.dll b/lib/14.0/Microsoft.TeamFoundation.Git.Provider.dll similarity index 100% rename from lib/Microsoft.TeamFoundation.Git.Provider.dll rename to lib/14.0/Microsoft.TeamFoundation.Git.Provider.dll diff --git a/lib/15.0/Microsoft.TeamFoundation.Client.dll b/lib/15.0/Microsoft.TeamFoundation.Client.dll new file mode 100644 index 0000000000..ff4164ee7f Binary files /dev/null and b/lib/15.0/Microsoft.TeamFoundation.Client.dll differ diff --git a/lib/15.0/Microsoft.TeamFoundation.Common.dll b/lib/15.0/Microsoft.TeamFoundation.Common.dll new file mode 100644 index 0000000000..7ab1fb2525 Binary files /dev/null and b/lib/15.0/Microsoft.TeamFoundation.Common.dll differ diff --git a/lib/15.0/Microsoft.TeamFoundation.Controls.dll b/lib/15.0/Microsoft.TeamFoundation.Controls.dll new file mode 100644 index 0000000000..65d4237f55 Binary files /dev/null and b/lib/15.0/Microsoft.TeamFoundation.Controls.dll differ diff --git a/lib/15.0/Microsoft.TeamFoundation.Git.Client.dll b/lib/15.0/Microsoft.TeamFoundation.Git.Client.dll new file mode 100644 index 0000000000..f77b7d6a56 Binary files /dev/null and b/lib/15.0/Microsoft.TeamFoundation.Git.Client.dll differ diff --git a/lib/15.0/Microsoft.TeamFoundation.Git.Controls.dll b/lib/15.0/Microsoft.TeamFoundation.Git.Controls.dll new file mode 100644 index 0000000000..d077a0816c Binary files /dev/null and b/lib/15.0/Microsoft.TeamFoundation.Git.Controls.dll differ diff --git a/lib/15.0/Microsoft.TeamFoundation.Git.Provider.dll b/lib/15.0/Microsoft.TeamFoundation.Git.Provider.dll new file mode 100644 index 0000000000..f8908bf622 Binary files /dev/null and b/lib/15.0/Microsoft.TeamFoundation.Git.Provider.dll differ diff --git a/lib/Markdig.Wpf.Signed.0.2.1.nupkg b/lib/Markdig.Wpf.Signed.0.2.1.nupkg new file mode 100644 index 0000000000..70561c66c4 Binary files /dev/null and b/lib/Markdig.Wpf.Signed.0.2.1.nupkg differ diff --git a/lib/Microsoft.TextTemplating.Build.Tasks.dll b/lib/Microsoft.TextTemplating.Build.Tasks.dll new file mode 100644 index 0000000000..7426b3b3e1 Binary files /dev/null and b/lib/Microsoft.TextTemplating.Build.Tasks.dll differ diff --git a/lib/Microsoft.TextTemplating.targets b/lib/Microsoft.TextTemplating.targets new file mode 100644 index 0000000000..f7727d5fc0 --- /dev/null +++ b/lib/Microsoft.TextTemplating.targets @@ -0,0 +1,535 @@ + + + + + + + + + + + + + $(T4BuildTasksPath)Microsoft.TextTemplating.Build.Tasks.dll + + + false + + + true + + + false + + + false + + + $(IntermediateOutputPath) + + true + + + + + + + false + + + + + + + + + + + + + + $(Registry:HKEY_LOCAL_MACHINE\Software\Microsoft\VisualStudio\14.0@InstallDir) + $(Registry:HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\VisualStudio\14.0@InstallDir) + + + $(VsIdePath)Extensions\Microsoft\DSL SDK\Dsl Designer\$(VisualStudioVersion)\ + + + $(IncludeFolders);$(DslDesignerInstallPath)TextTemplates\ + + + + + + Microsoft.VisualStudio.Modeling.DslDefinition.DslDirectiveProcessor + + $(Registry:HKEY_LOCAL_MACHINE\Software\Microsoft\VisualStudio\14.0@InstallDir)Extensions\Microsoft\DSL SDK\DSL Designer\$(VisualStudioVersion)\Microsoft.VisualStudio.Modeling.Sdk.DslDefinition.$(VisualStudioVersion).dll + + + + + + + + $(RootNamespace) + + + + + $(T4DslToolsNamespace) + + + + + + + + + + + + + + + + + + + + + + + ProcessTransformResults + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextTemplatingFileGenerator + TextTemplatingFilePreprocessor + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(RootNamespace) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TransformDuringBuild;$(BuildDependsOn) + + + + + + + + + diff --git a/lib/Microsoft.VisualStudio.TextTemplating.Sdk.Host.14.0.dll b/lib/Microsoft.VisualStudio.TextTemplating.Sdk.Host.14.0.dll new file mode 100644 index 0000000000..64579110ca Binary files /dev/null and b/lib/Microsoft.VisualStudio.TextTemplating.Sdk.Host.14.0.dll differ diff --git a/lib/Octokit.GraphQL.0.1.1-beta.nupkg b/lib/Octokit.GraphQL.0.1.1-beta.nupkg new file mode 100644 index 0000000000..ac26783672 Binary files /dev/null and b/lib/Octokit.GraphQL.0.1.1-beta.nupkg differ diff --git a/lib/Rothko.0.0.3-ghfvs.nupkg b/lib/Rothko.0.0.3-ghfvs.nupkg new file mode 100644 index 0000000000..522e3c7b54 Binary files /dev/null and b/lib/Rothko.0.0.3-ghfvs.nupkg differ diff --git a/nuget.config b/nuget.config index ac070021ef..f9e15995d3 100644 --- a/nuget.config +++ b/nuget.config @@ -7,4 +7,7 @@ - \ No newline at end of file + + + + diff --git a/script b/script index e8bd8e1bfd..5ed9b3d7bc 160000 --- a/script +++ b/script @@ -1 +1 @@ -Subproject commit e8bd8e1bfdff5d3355f884af1e6f3b9f7f26716f +Subproject commit 5ed9b3d7bceee50d27a4a5838d4c0265bd35cc8e diff --git a/scripts/Bump-Version.ps1 b/scripts/Bump-Version.ps1 new file mode 100644 index 0000000000..33a89cac8c --- /dev/null +++ b/scripts/Bump-Version.ps1 @@ -0,0 +1,92 @@ +<# +.SYNOPSIS + Bumps the version number of GitHub for Visual Studio +.DESCRIPTION + By default, just bumps the last component of the version number by one. An + alternate version number can be specified on the command line. + + The new version number is committed to the local repository and pushed to + GitHub. +#> + +Param( + # It would be nice to use our Validate-Version function here, but we + # can't because this Param definition has to come before any other code in the + # file. + [ValidateScript({ ($_.Major -ge 0) -and ($_.Minor -ge 0) -and ($_.Build -ge 0) })] + [System.Version] + $NewVersion = $null + , + [switch] + $BumpMajor = $false + , + [switch] + $BumpMinor = $false + , + [switch] + $BumpPatch = $false + , + [switch] + $BumpBuild = $false + , + [int] + $BuildNumber = -1 + , + [switch] + $Commit = $false + , + [switch] + $Push = $false + , + [switch] + $Force = $false + , + [switch] + $Trace = $false +) + +Set-StrictMode -Version Latest +if ($Trace) { Set-PSDebug -Trace 1 } + +. $PSScriptRoot\modules.ps1 | out-null +. $scriptsDirectory\Modules\Versioning.ps1 | out-null +. $scriptsDirectory\Modules\Vsix.ps1 | out-null +. $scriptsDirectory\Modules\SolutionInfo.ps1 | out-null +. $scriptsDirectory\Modules\AppVeyor.ps1 | out-null + +if ($NewVersion -eq $null) { + if (!$BumpMajor -and !$BumpMinor -and !$BumpPatch -and !$BumpBuild){ + Die -1 "You need to indicate which part of the version to update via -BumpMajor/-BumpMinor/-BumpPatch/-BumpBuild flags or a custom version via -NewVersion" + } +} + +if ($Push -and !$Commit) { + Die 1 "Cannot push a version bump without -Commit" +} + +if ($Commit -and !$Force){ + Require-CleanWorkTree "bump version" +} + +if (!$?) { + exit 1 +} + +if ($NewVersion -eq $null) { + $currentVersion = Read-Version + $NewVersion = Generate-Version $currentVersion $BumpMajor $BumpMinor $BumpPatch $BumpBuild $BuildNumber +} + +Write-Output "Setting version to $NewVersion" +Write-Version $NewVersion + +if ($Commit) { + Write-Output "Committing version change" + Commit-Version $NewVersion + + if ($Push) { + Write-Output "Pushing version change" + $branch = & $git rev-parse --abbrev-ref HEAD + Push-Changes $branch + } +} diff --git a/scripts/Get-CheckedOutBranch.ps1 b/scripts/Get-CheckedOutBranch.ps1 new file mode 100644 index 0000000000..38a961c2e3 --- /dev/null +++ b/scripts/Get-CheckedOutBranch.ps1 @@ -0,0 +1,31 @@ +<# +.SYNOPSIS + Returns the name of the working directory's currently checked-out branch +#> + +Set-PSDebug -Strict + +$scriptsDirectory = Split-Path $MyInvocation.MyCommand.Path +$rootDirectory = Split-Path $scriptsDirectory + +. $scriptsDirectory\common.ps1 + +function Die([string]$message, [object[]]$output) { + if ($output) { + Write-Output $output + $message += ". See output above." + } + Write-Error $message + exit 1 +} + +$output = & $git symbolic-ref HEAD 2>&1 | %{ "$_" } +if (!$? -or ($LastExitCode -ne 0)) { + Die "Failed to determine current branch" $output +} + +if (!($output -match "^refs/heads/(\S+)$")) { + Die "Failed to determine current branch. HEAD is $output" $output +} + +$matches[1] diff --git a/scripts/Require-CleanWorkTree.ps1 b/scripts/Require-CleanWorkTree.ps1 new file mode 100644 index 0000000000..741a05ab26 --- /dev/null +++ b/scripts/Require-CleanWorkTree.ps1 @@ -0,0 +1,57 @@ +<# +.SYNOPSIS + Ensures the working tree has no uncommitted changes +.PARAMETER Action + The action that requires a clean work tree. This will appear in error messages. +.PARAMETER WarnOnly + When true, warns rather than dies when uncommitted changes are found. +#> + +[CmdletBinding()] +Param( + [ValidateNotNullOrEmpty()] + [string] + $Action + , + [switch] + $WarnOnly = $false +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +. $PSScriptRoot\modules.ps1 | out-null + +# Based on git-sh-setup.sh:require_clean_work_tree in git.git, but changed not +# to ignore submodules. + +Push-Location $rootDirectory + +Run-Command -Fatal { & $git rev-parse --verify HEAD | Out-Null } + +& $git update-index -q --refresh + +& $git diff-files --quiet +$error = "" +if ($LastExitCode -ne 0) { + $error = "You have unstaged changes." +} + +& $git diff-index --cached --quiet HEAD -- +if ($LastExitCode -ne 0) { + if ($error) { + $error += " Additionally, your index contains uncommitted changes." + } else { + $error = "Your index contains uncommitted changes." + } +} + +if ($error) { + if ($WarnOnly) { + Write-Warning "$error Continuing anyway." + } else { + Die 2 ("Cannot $Action" + ": $error") + } +} + +Pop-Location diff --git a/scripts/Run-NUnit.ps1 b/scripts/Run-NUnit.ps1 new file mode 100644 index 0000000000..ac4662198a --- /dev/null +++ b/scripts/Run-NUnit.ps1 @@ -0,0 +1,54 @@ +<# +.SYNOPSIS + Runs NUnit +#> + +[CmdletBinding()] +Param( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string] + $BasePathToProject + , + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string] + $Project + , + [int] + $TimeoutDuration + , + [string] + $Configuration + , + [switch] + $AppVeyor = $false +) + +$scriptsDirectory = $PSScriptRoot +$rootDirectory = Split-Path ($scriptsDirectory) +. $scriptsDirectory\modules.ps1 | out-null + +$dll = "$BasePathToProject\$Project\bin\$Configuration\$Project.dll" +$nunitDirectory = Join-Path $rootDirectory packages\NUnit.ConsoleRunner.3.7.0\tools +$consoleRunner = Join-Path $nunitDirectory nunit3-console.exe +$xml = Join-Path $rootDirectory "nunit-$Project.xml" + +& { + Trap { + Write-Output "$Project tests failed" + exit -1 + } + + $args = @() + if ($AppVeyor) { + $args = $dll, "--where", "cat!=Timings", "--result=$xml;format=AppVeyor" + } else { + $args = $dll, "--where", "cat!=Timings", "--result=$xml" + } + + Run-Process -Fatal $TimeoutDuration $consoleRunner $args + if (!$?) { + Die 1 "$Project tests failed" + } +} diff --git a/scripts/build.ps1 b/scripts/build.ps1 new file mode 100644 index 0000000000..aa44a88934 --- /dev/null +++ b/scripts/build.ps1 @@ -0,0 +1,84 @@ +<# +.SYNOPSIS + Builds and (optionally) runs tests for GitHub for Visual Studio +.DESCRIPTION + Build GHfVS +.PARAMETER Clean + When true, all untracked (and ignored) files will be removed from the work + tree and all submodules. Defaults to false. +.PARAMETER Config + Debug or Release +.PARAMETER RunTests + Runs the tests (defauls to false) +#> +[CmdletBinding()] + +Param( + [switch] + $UpdateSubmodules = $false + , + [switch] + $Clean = $false + , + [ValidateSet('Debug', 'Release')] + [string] + $Config = "Release" + , + [switch] + $Package = $false + , + [switch] + $AppVeyor = $false + , + [switch] + $BumpVersion = $false + , + [int] + $BuildNumber = -1 + , + [switch] + $Trace = $false +) + +Set-StrictMode -Version Latest +if ($Trace) { + Set-PSDebug -Trace 1 +} + +. $PSScriptRoot\modules.ps1 | out-null +$env:PATH = "$scriptsDirectory;$scriptsDirectory\Modules;$env:PATH" + +Import-Module $scriptsDirectory\Modules\Debugging.psm1 +Vsix | out-null + +Push-Location $rootDirectory + +if ($UpdateSubmodules) { + Update-Submodules +} + +if ($Clean) { + Clean-WorkingTree +} + +$fullBuild = Test-Path env:GHFVS_KEY +$publishable = $fullBuild -and $AppVeyor -and ($env:APPVEYOR_PULL_REQUEST_NUMBER -or $env:APPVEYOR_REPO_BRANCH -eq "master") +if ($publishable) { #forcing a deploy flag for CI + $Package = $true + $BumpVersion = $true +} + +if ($BumpVersion) { + Write-Output "Bumping the version" + Bump-Version -BumpBuild -BuildNumber:$BuildNumber +} + +if ($Package) { + Write-Output "Building and packaging GitHub for Visual Studio" +} else { + Write-Output "Building GitHub for Visual Studio" +} + +Build-Solution GitHubVs.sln "Build" $config -Deploy:$Package + +Pop-Location diff --git a/scripts/clearerror.cmd b/scripts/clearerror.cmd new file mode 100644 index 0000000000..9a18480a67 --- /dev/null +++ b/scripts/clearerror.cmd @@ -0,0 +1 @@ +@echo off \ No newline at end of file diff --git a/scripts/common.ps1 b/scripts/common.ps1 new file mode 100644 index 0000000000..3637124792 --- /dev/null +++ b/scripts/common.ps1 @@ -0,0 +1,69 @@ +$scriptsDirectory = Split-Path $MyInvocation.MyCommand.Path +$rootDirectory = Split-Path ($scriptsDirectory) + +function Die([string]$message, [object[]]$output) { + if ($output) { + Write-Output $output + $message += ". See output above." + } + Throw (New-Object -TypeName ScriptException -ArgumentList $message) +} + +if (Test-Path "C:\Program Files (x86)\MSBuild\14.0\Bin\MSBuild.exe") { + $msbuild = "C:\Program Files (x86)\MSBuild\14.0\Bin\MSBuild.exe" +} +elseif (Test-Path "C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe") { + $msbuild = "C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe" +} +else { + Die("No suitable msbuild.exe found.") +} + +$git = (Get-Command 'git.exe').Path +if (!$git) { + $git = Join-Path $rootDirectory 'PortableGit\cmd\git.exe' +} +if (!$git) { + throw "Couldn't find installed an git.exe" +} + +$nuget = Join-Path $rootDirectory "tools\nuget\nuget.exe" + +function Create-TempDirectory { + $path = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName()) + New-Item -Type Directory $path +} + +function Build-Solution([string]$solution,[string]$target,[string]$configuration, [bool]$ForVSInstaller) { + Run-Command -Fatal { & $nuget restore $solution -NonInteractive -Verbosity detailed } + $flag1 = "" + $flag2 = "" + if ($ForVSInstaller) { + $flag1 = "/p:IsProductComponent=true" + $flag2 = "/p:TargetVsixContainer=$rootDirectory\build\vsinstaller\GitHub.VisualStudio.vsix" + new-item -Path $rootDirectory\build\vsinstaller -ItemType Directory -Force | Out-Null + } + + Write-Output "$msbuild $solution /target:$target /property:Configuration=$configuration /p:DeployExtension=false /verbosity:minimal /p:VisualStudioVersion=14.0 $flag1 $flag2" + Run-Command -Fatal { & $msbuild $solution /target:$target /property:Configuration=$configuration /p:DeployExtension=false /verbosity:minimal /p:VisualStudioVersion=14.0 $flag1 $flag2 } +} + +function Push-Changes([string]$branch) { + Push-Location $rootDirectory + + Write-Output "Pushing $Branch to GitHub..." + + Run-Command -Fatal { & $git push origin $branch } + + Pop-Location +} + +Add-Type -AssemblyName "System.Core" +Add-Type -TypeDefinition @" +public class ScriptException : System.Exception +{ + public ScriptException(string message) : base(message) + { + } +} +"@ diff --git a/scripts/modules.ps1 b/scripts/modules.ps1 new file mode 100644 index 0000000000..f0430ddc49 --- /dev/null +++ b/scripts/modules.ps1 @@ -0,0 +1,199 @@ +Add-Type -AssemblyName "System.Core" +Add-Type -TypeDefinition @" +public class ScriptException : System.Exception +{ + public int ExitCode { get; private set; } + public ScriptException(string message, int exitCode) : base(message) + { + this.ExitCode = exitCode; + } +} +"@ + +New-Module -ScriptBlock { + $rootDirectory = Split-Path ($PSScriptRoot) + $scriptsDirectory = Join-Path $rootDirectory "scripts" + $nuget = Join-Path $rootDirectory "tools\nuget\nuget.exe" + Export-ModuleMember -Variable scriptsDirectory,rootDirectory,nuget +} + +New-Module -ScriptBlock { + function Die([int]$exitCode, [string]$message, [object[]]$output) { + #$host.SetShouldExit($exitCode) + if ($output) { + Write-Host $output + $message += ". See output above." + } + $hash = @{ + Message = $message + ExitCode = $exitCode + Output = $output + } + Throw (New-Object -TypeName ScriptException -ArgumentList $message,$exitCode) + #throw $message + } + + + function Run-Command([scriptblock]$Command, [switch]$Fatal, [switch]$Quiet) { + $output = "" + + $exitCode = 0 + + if ($Quiet) { + $output = & $command 2>&1 | %{ "$_" } + } else { + & $command + } + + if (!$? -and $LastExitCode -ne 0) { + $exitCode = $LastExitCode + } elseif ($? -and $LastExitCode -ne 0) { + $exitCode = $LastExitCode + } + + if ($exitCode -ne 0) { + if (!$Fatal) { + Write-Host "``$Command`` failed" $output + } else { + Die $exitCode "``$Command`` failed" $output + } + } + $output + } + + function Run-Process([int]$Timeout, [string]$Command, [string[]]$Arguments, [switch]$Fatal = $false) + { + $args = ($Arguments | %{ "`"$_`"" }) + [object[]] $output = "$Command " + $args + $exitCode = 0 + $outputPath = [System.IO.Path]::GetTempFileName() + $process = Start-Process -PassThru -NoNewWindow -RedirectStandardOutput $outputPath $Command ($args | %{ "`"$_`"" }) + Wait-Process -InputObject $process -Timeout $Timeout -ErrorAction SilentlyContinue + if ($process.HasExited) { + $output += Get-Content $outputPath + $exitCode = $process.ExitCode + } else { + $output += "Tests timed out. Backtrace:" + $output += Get-DotNetStack $process.Id + $exitCode = 9999 + } + Stop-Process -InputObject $process + Remove-Item $outputPath + if ($exitCode -ne 0) { + if (!$Fatal) { + Write-Host "``$Command`` failed" $output + } else { + Die $exitCode "``$Command`` failed" $output + } + } + $output + } + + function Create-TempDirectory { + $path = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName()) + New-Item -Type Directory $path + } + + Export-ModuleMember -Function Die,Run-Command,Run-Process,Create-TempDirectory +} + +New-Module -ScriptBlock { + function Find-MSBuild() { + if (Test-Path "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\bin\MSBuild.exe") { + $msbuild = "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\bin\MSBuild.exe" + } + elseif (Test-Path "C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe") { + $msbuild = "C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe" + } + else { + Die("No suitable msbuild.exe found.") + } + $msbuild + } + + function Build-Solution([string]$solution, [string]$target, [string]$configuration, [switch]$ForVSInstaller, [bool]$Deploy = $false) { + Run-Command -Fatal { & $nuget restore $solution -NonInteractive -Verbosity detailed } + $flag1 = "" + $flag2 = "" + if ($ForVSInstaller) { + $flag1 = "/p:IsProductComponent=true" + $flag2 = "/p:TargetVsixContainer=$rootDirectory\build\vsinstaller\GitHub.VisualStudio.vsix" + new-item -Path $rootDirectory\build\vsinstaller -ItemType Directory -Force | Out-Null + } elseif (!$Deploy) { + $configuration += "WithoutVsix" + $flag1 = "/p:Package=Skip" + } + + $msbuild = Find-MSBuild + + Write-Host "$msbuild $solution /target:$target /property:Configuration=$configuration /p:DeployExtension=false /verbosity:minimal /p:VisualStudioVersion=14.0 $flag1 $flag2" + Run-Command -Fatal { & $msbuild $solution /target:$target /property:Configuration=$configuration /p:DeployExtension=false /verbosity:minimal /p:VisualStudioVersion=14.0 $flag1 $flag2 } + } + + Export-ModuleMember -Function Find-MSBuild,Build-Solution +} + +New-Module -ScriptBlock { + function Find-Git() { + $git = (Get-Command 'git.exe').Path + if (!$git) { + $git = Join-Path $rootDirectory 'PortableGit\cmd\git.exe' + } + if (!$git) { + Die("Couldn't find installed an git.exe") + } + $git + } + + function Push-Changes([string]$branch) { + Push-Location $rootDirectory + + Write-Host "Pushing $Branch to GitHub..." + + Run-Command -Fatal { & $git push origin $branch } + + Pop-Location + } + + function Update-Submodules { + Write-Host "Updating submodules..." + Write-Host "" + + Run-Command -Fatal { git submodule init } + Run-Command -Fatal { git submodule sync } + Run-Command -Fatal { git submodule update --recursive --force } + } + + function Clean-WorkingTree { + Write-Host "Cleaning work tree..." + Write-Host "" + + Run-Command -Fatal { git clean -xdf } + Run-Command -Fatal { git submodule foreach git clean -xdf } + } + + function Get-HeadSha { + Run-Command -Quiet { & $git rev-parse HEAD } + } + + $git = Find-Git + Export-ModuleMember -Function Find-Git,Push-Changes,Update-Submodules,Clean-WorkingTree,Get-HeadSha +} + +New-Module -ScriptBlock { + function Write-Manifest([string]$directory) { + Add-Type -Path (Join-Path $rootDirectory packages\Newtonsoft.Json.6.0.8\lib\net35\Newtonsoft.Json.dll) + + $manifest = @{ + NewestExtension = @{ + Version = [string](Read-CurrentVersionVsix) + Commit = [string](Get-HeadSha) + } + } + + $manifestPath = Join-Path $directory manifest + [Newtonsoft.Json.JsonConvert]::SerializeObject($manifest) | Out-File $manifestPath -Encoding UTF8 + } + + Export-ModuleMember -Function Write-Manifest +} \ No newline at end of file diff --git a/scripts/modules/AppVeyor.ps1 b/scripts/modules/AppVeyor.ps1 new file mode 100644 index 0000000000..49470283d0 --- /dev/null +++ b/scripts/modules/AppVeyor.ps1 @@ -0,0 +1,41 @@ +Set-StrictMode -Version Latest + +New-Module -ScriptBlock { + + function Get-AppVeyorPath { + Join-Path $rootDirectory appveyor.yml + } + + function Read-VersionAppVeyor { + $file = Get-AppVeyorPath + $currentVersion = Get-Content $file | %{ + $regex = "`^version: '(\d+\.\d+\.\d+)\.`{build`}'`$" + if ($_ -match $regex) { + $matches[1] + } + } + [System.Version] $currentVersion + } + + function Write-VersionAppVeyor([System.Version]$version) { + $file = Get-AppVeyorPath + $numberOfReplacements = 0 + $newContent = Get-Content $file | %{ + $newString = $_ + $regex = "version: '(\d+\.\d+\.\d+)" + if ($newString -match $regex) { + $numberOfReplacements++ + $newString = $newString -replace $regex, "version: '$($version.Major).$($version.Minor).$($version.Build)" + } + $newString + } + + if ($numberOfReplacements -ne 1) { + Die 1 "Expected to replace the version number in 1 place in appveyor.yml (version) but actually replaced it in $numberOfReplacements" + } + + $newContent | Set-Content $file + } + + Export-ModuleMember -Function Get-AppVeyorPath,Read-VersionAppVeyor,Write-VersionAppVeyor +} \ No newline at end of file diff --git a/scripts/modules/BuildUtils.psm1 b/scripts/modules/BuildUtils.psm1 new file mode 100644 index 0000000000..f93d6eecb2 --- /dev/null +++ b/scripts/modules/BuildUtils.psm1 @@ -0,0 +1,18 @@ +Set-StrictMode -Version Latest + +function Update-Submodules { + Write-Output "Updating submodules..." + Write-Output "" + + Run-Command -Fatal { git submodule init } + Run-Command -Fatal { git submodule sync } + Run-Command -Fatal { git submodule update --recursive --force } +} + +function Clean-WorkingTree { + Write-Output "Cleaning work tree..." + Write-Output "" + + Run-Command -Fatal { git clean -xdf } + Run-Command -Fatal { git submodule foreach git clean -xdf } +} \ No newline at end of file diff --git a/scripts/modules/Debugging.psm1 b/scripts/modules/Debugging.psm1 new file mode 100644 index 0000000000..2ca851ec0a --- /dev/null +++ b/scripts/modules/Debugging.psm1 @@ -0,0 +1,26 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$rootDirectory = Split-Path (Split-Path (Split-Path $MyInvocation.MyCommand.Path)) +$cdb = Join-Path $rootDirectory "tools\Debugging Tools for Windows\cdb.exe" + +function Get-DotNetStack([int]$ProcessId) { + $commands = @( + ".cordll -ve -u -l", + ".loadby sos clr", + "!eestack -ee", + ".detach", + "q" + ) + + $Env:_NT_SYMBOL_PATH = "cache*${Env:PROGRAMDATA}\dbg\sym;SRV*http://msdl.microsoft.com/download/symbols;srv*http://windows-symbols.githubapp.com/symbols" + $output = & $cdb -lines -p $ProcessId -c ($commands -join "; ") + if ($LastExitCode -ne 0) { + $output + throw "Error running cdb" + } + + $start = ($output | Select-String -List -Pattern "^Thread 0").LineNumber - 1 + $end = ($output | Select-String -List -Pattern "^Detached").LineNumber - 2 + $output[$start..$end] +} diff --git a/scripts/modules/SolutionInfo.ps1 b/scripts/modules/SolutionInfo.ps1 new file mode 100644 index 0000000000..4e1d6e1d0f --- /dev/null +++ b/scripts/modules/SolutionInfo.ps1 @@ -0,0 +1,41 @@ +Set-StrictMode -Version Latest + +New-Module -ScriptBlock { + + function Get-SolutionInfoPath { + Join-Path $rootDirectory src\common\SolutionInfo.cs + } + + function Read-VersionSolutionInfo { + $file = Get-SolutionInfoPath + $currentVersion = Get-Content $file | %{ + $regex = "const string Version = `"(\d+\.\d+\.\d+\.\d+)`";" + if ($_ -match $regex) { + $matches[1] + } + } + [System.Version] $currentVersion + } + + function Write-VersionSolutionInfo([System.Version]$version) { + $file = Get-SolutionInfoPath + $numberOfReplacements = 0 + $newContent = Get-Content $file | %{ + $newString = $_ + $regex = "(string Version = `")\d+\.\d+\.\d+\.\d+" + if ($_ -match $regex) { + $numberOfReplacements++ + $newString = $newString -replace $regex, "string Version = `"$version" + } + $newString + } + + if ($numberOfReplacements -ne 1) { + Die 1 "Expected to replace the version number in 1 place in SolutionInfo.cs (Version) but actually replaced it in $numberOfReplacements" + } + + $newContent | Set-Content $file + } + + Export-ModuleMember -Function Get-SolutionInfoPath,Read-VersionSolutionInfo,Write-VersionSolutionInfo +} \ No newline at end of file diff --git a/scripts/modules/Versioning.ps1 b/scripts/modules/Versioning.ps1 new file mode 100644 index 0000000000..22f94a8656 --- /dev/null +++ b/scripts/modules/Versioning.ps1 @@ -0,0 +1,68 @@ +Set-StrictMode -Version Latest + +New-Module -ScriptBlock { + + function Validate-Version([System.Version]$version) { + ($version.Major -ge 0) -and ($version.Minor -ge 0) -and ($version.Build -ge 0) + } + + function Generate-Version([System.Version]$currentVersion, + [bool]$BumpMajor, [bool] $BumpMinor, + [bool]$BumpPatch, [bool] $BumpBuild, + [int]$BuildNumber = -1) { + + if (!(Validate-Version $currentVersion)) { + Die 1 "Invalid current version $currentVersion" + } + + if ($BumpMajor) { + New-Object -TypeName System.Version -ArgumentList ($currentVersion.Major + 1), $currentVersion.Minor, $currentVersion.Build, 0 + } elseif ($BumpMinor) { + New-Object -TypeName System.Version -ArgumentList $currentVersion.Major, ($currentVersion.Minor + 1), $currentVersion.Build, 0 + } elseif ($BumpPatch) { + New-Object -TypeName System.Version -ArgumentList $currentVersion.Major, $currentVersion.Minor, ($currentVersion.Build + 1), 0 + } elseif ($BumpBuild) { + if ($BuildNumber -ge 0) { + [System.Version] "$($currentVersion.Major).$($currentVersion.Minor).$($currentVersion.Build).$BuildNumber" + } else { + $timestamp = [System.DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + [System.Version] "$($currentVersion.Major).$($currentVersion.Minor).$($currentVersion.Build).$timestamp" + } + } + else { + $currentVersion + } + } + + function Read-Version { + Read-VersionAppVeyor + } + + function Write-Version([System.Version]$version) { + Write-VersionVsixManifest $version + Write-VersionSolutionInfo $version + Write-VersionAppVeyor $version + Push-Location $rootDirectory + New-Item -Type Directory -ErrorAction SilentlyContinue build | out-null + Set-Content build\version $version + Pop-Location + } + + function Commit-Version([System.Version]$version) { + + Write-Host "Committing version bump..." + + Push-Location $rootDirectory + + Run-Command -Fatal { & $git commit --message "Bump version to $version" -- } + + $output = Start-Process $git "commit --all --message ""Bump version to $version""" -wait -NoNewWindow -ErrorAction Continue -PassThru + if ($output.ExitCode -ne 0) { + Die 1 "Error committing version bump" + } + + Pop-Location + } + + Export-ModuleMember -Function Validate-Version,Write-Version,Commit-Version,Generate-Version,Read-Version +} diff --git a/scripts/modules/Vsix.ps1 b/scripts/modules/Vsix.ps1 new file mode 100644 index 0000000000..63563d3f00 --- /dev/null +++ b/scripts/modules/Vsix.ps1 @@ -0,0 +1,35 @@ +Set-StrictMode -Version Latest + +New-Module -ScriptBlock { + $gitHubDirectory = Join-Path $rootDirectory src\GitHub.VisualStudio + + function Get-VsixManifestPath { + Join-Path $gitHubDirectory source.extension.vsixmanifest + } + + function Get-VsixManifestXml { + $xmlLines = Get-Content (Get-VsixManifestPath) + # If we don't explicitly join the lines with CRLF, comments in the XML will + # end up with LF line-endings, which will make Git spew a warning when we + # try to commit the version bump. + $xmlText = $xmlLines -join [System.Environment]::NewLine + + [xml] $xmlText + } + + function Read-CurrentVersionVsix { + [System.Version] (Get-VsixManifestXml).PackageManifest.Metadata.Identity.Version + } + + function Write-VersionVsixManifest([System.Version]$version) { + + $document = Get-VsixManifestXml + + $numberOfReplacements = 0 + $document.PackageManifest.Metadata.Identity.Version = $version.ToString() + + $document.Save((Get-VsixManifestPath)) + } + + Export-ModuleMember -Function Read-CurrentVersionVsix,Write-VersionVsixManifest +} \ No newline at end of file diff --git a/scripts/test.ps1 b/scripts/test.ps1 new file mode 100644 index 0000000000..6e64d9df63 --- /dev/null +++ b/scripts/test.ps1 @@ -0,0 +1,103 @@ +<# +.SYNOPSIS + Runs tests for GitHub for Visual Studio +.DESCRIPTION + Build GHfVS +.PARAMETER Clean + When true, all untracked (and ignored) files will be removed from the work + tree and all submodules. Defaults to false. +#> +[CmdletBinding()] + +Param( + [ValidateSet('Debug', 'Release')] + [string] + $Config = "Release" + , + [int] + $TimeoutDuration = 180 + , + [switch] + $Trace = $false + +) + +Set-StrictMode -Version Latest +if ($Trace) { + Set-PSDebug -Trace 1 +} + +$env:PATH = "$PSScriptRoot;$env:PATH" + +$exitcode = 0 + +Write-Output "Running Tracking Collection Tests..." +Run-NUnit test TrackingCollectionTests $TimeoutDuration $config +if (!$?) { + $exitcode = 1 +} + +Write-Output "Running GitHub.Api.UnitTests..." +Run-NUnit test GitHub.Api.UnitTests $TimeoutDuration $config +if (!$?) { + $exitcode = 2 +} + +Write-Output "Running GitHub.App.UnitTests..." +Run-NUnit test GitHub.App.UnitTests $TimeoutDuration $config +if (!$?) { + $exitcode = 3 +} + +Write-Output "Running GitHub.Exports.Reactive.UnitTests..." +Run-NUnit test GitHub.Exports.Reactive.UnitTests $TimeoutDuration $config +if (!$?) { + $exitcode = 4 +} + +Write-Output "Running GitHub.Exports.UnitTests..." +Run-NUnit test GitHub.Exports.UnitTests $TimeoutDuration $config +if (!$?) { + $exitcode = 5 +} + +Write-Output "Running GitHub.Extensions.UnitTests..." +Run-NUnit test GitHub.Extensions.UnitTests $TimeoutDuration $config +if (!$?) { + $exitcode = 6 +} + +Write-Output "Running GitHub.Primitives.UnitTests..." +Run-NUnit test GitHub.Primitives.UnitTests $TimeoutDuration $config +if (!$?) { + $exitcode = 7 +} + +Write-Output "Running GitHub.TeamFoundation.UnitTests..." +Run-NUnit test GitHub.TeamFoundation.UnitTests $TimeoutDuration $config +if (!$?) { + $exitcode = 8 +} + +Write-Output "Running GitHub.UI.UnitTests..." +Run-NUnit test GitHub.UI.UnitTests $TimeoutDuration $config +if (!$?) { + $exitcode = 9 +} + +Write-Output "Running GitHub.VisualStudio.UnitTests..." +Run-NUnit test GitHub.VisualStudio.UnitTests $TimeoutDuration $config +if (!$?) { + $exitcode = 10 +} + +Write-Output "Running GitHub.InlineReviews.UnitTests..." +Run-NUnit test GitHub.InlineReviews.UnitTests $TimeoutDuration $config +if (!$?) { + $exitcode = 11 +} + +if ($exitcode -ne 0) { + $host.SetShouldExit($exitcode) +} +exit $exitcode \ No newline at end of file diff --git a/signingkey.snk b/signingkey.snk new file mode 100644 index 0000000000..371008d5a6 Binary files /dev/null and b/signingkey.snk differ diff --git a/src/CredentialManagement/Credential.cs b/src/CredentialManagement/Credential.cs index 56618c3256..170bd479d0 100644 --- a/src/CredentialManagement/Credential.cs +++ b/src/CredentialManagement/Credential.cs @@ -4,7 +4,7 @@ using System.Security; using System.Security.Permissions; using System.Text; -using NullGuard; +using GitHub.Extensions; namespace GitHub.Authentication.CredentialManagement { @@ -35,9 +35,9 @@ public Credential() : this(null, (string)null) {} public Credential( - [AllowNull]string username, - [AllowNull]SecureString password, - [AllowNull]string target = null) + string username, + SecureString password, + string target = null) { Username = username; SecurePassword = password; @@ -48,18 +48,40 @@ public Credential( } public Credential( - [AllowNull]string username, - [AllowNull]string password, - [AllowNull]string target = null) + string username, + string password, + string target = null) { Username = username; Password = password; Target = target; Type = CredentialType.Generic; - PersistenceType = PersistenceType.Session; + PersistenceType = PersistenceType.LocalComputer; _lastWriteTime = DateTime.MinValue; } + public static Credential Load(string key) + { + var result = new Credential(); + result.Target = key; + result.Type = CredentialType.Generic; + return result.Load() ? result : null; + } + + public static void Save(string key, string username, string password) + { + var result = new Credential(username, password, key); + result.Save(); + } + + public static void Delete(string key) + { + var result = new Credential(); + result.Target = key; + result.Type = CredentialType.Generic; + result.Delete(); + } + bool disposed; void Dispose(bool disposing) { @@ -86,10 +108,8 @@ private void CheckNotDisposed() } } - [AllowNull] public string Username { - [return: AllowNull] get { CheckNotDisposed(); @@ -102,10 +122,8 @@ public string Username } } - [AllowNull] public string Password { - [return: AllowNull] get { return SecureStringHelper.CreateString(SecurePassword); @@ -117,7 +135,6 @@ public string Password } } - [AllowNull] public SecureString SecurePassword { get @@ -138,10 +155,8 @@ public SecureString SecurePassword } } - [AllowNull] public string Target { - [return: AllowNull] get { CheckNotDisposed(); @@ -154,10 +169,8 @@ public string Target } } - [AllowNull] public string Description { - [return: AllowNull] get { CheckNotDisposed(); @@ -245,6 +258,8 @@ public bool Save() public bool Save(byte[] passwordBytes) { + Guard.ArgumentNotNull(passwordBytes, nameof(passwordBytes)); + CheckNotDisposed(); _unmanagedCodePermission.Demand(); @@ -340,6 +355,8 @@ internal void LoadInternal(NativeMethods.CREDENTIAL credential) static void ValidatePasswordLength(byte[] passwordBytes) { + Guard.ArgumentNotNull(passwordBytes, nameof(passwordBytes)); + if (passwordBytes.Length > maxPasswordLengthInBytes) { var message = string.Format(CultureInfo.InvariantCulture, diff --git a/src/CredentialManagement/CredentialManagement.csproj b/src/CredentialManagement/CredentialManagement.csproj index d6d3e5685d..07648cf7aa 100644 --- a/src/CredentialManagement/CredentialManagement.csproj +++ b/src/CredentialManagement/CredentialManagement.csproj @@ -9,40 +9,44 @@ Properties CredentialManagement GitHub.CredentialManagement - v4.5 + 7.3 + v4.6.1 512 - - - - Internal + ..\common\GitHubVS.ruleset + true + true true full false - bin\Debug\ DEBUG;TRACE prompt 4 + false + bin\Debug\ + + + true + full + false + CODE_ANALYSIS;DEBUG;TRACE + prompt + 4 + true + bin\Debug\ pdbonly true - bin\Release\ TRACE prompt 4 + true + bin\Release\ - - ..\..\script\Key.snk - true - false - + - - ..\..\packages\NullGuard.Fody.1.4.1\Lib\portable-net4+sl4+wp7+win8+MonoAndroid16+MonoTouch40\NullGuard.dll - True - @@ -53,9 +57,6 @@ - - Key.snk - Properties\SolutionInfo.cs @@ -67,12 +68,6 @@ - - - - - - {6afe2e2d-6db0-4430-a2ea-f5f5388d2f78} @@ -80,13 +75,6 @@ - - - - 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}. - - - - \ No newline at end of file diff --git a/src/DesignTimeStyleHelper/MainWindow.xaml b/src/DesignTimeStyleHelper/MainWindow.xaml deleted file mode 100644 index 5e3283aa83..0000000000 --- a/src/DesignTimeStyleHelper/MainWindow.xaml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - Login - Clone - Create - Publish - Two Factor - - - - - - - - - diff --git a/src/DesignTimeStyleHelper/MainWindow.xaml.cs b/src/DesignTimeStyleHelper/MainWindow.xaml.cs deleted file mode 100644 index 9ca122416f..0000000000 --- a/src/DesignTimeStyleHelper/MainWindow.xaml.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Windows; -using GitHub.SampleData; -using GitHub.Services; -using GitHub.UI; -using GitHub.Extensions; -using GitHub.Models; - -namespace DesignTimeStyleHelper -{ - /// - /// Interaction logic for MainWindow.xaml - /// - public partial class MainWindow : Window - { - public MainWindow() - { - InitializeComponent(); - gitHubHomeSection.DataContext = new GitHubHomeSectionDesigner(); - } - - private void loginLink_Click(object sender, RoutedEventArgs e) - { - ShowDialog(UIControllerFlow.Authentication); - } - - private void cloneLink_Click(object sender, RoutedEventArgs e) - { - ShowDialog(UIControllerFlow.Clone); - } - - private void createLink_Click(object sender, RoutedEventArgs e) - { - ShowDialog(UIControllerFlow.Create); - } - - private void publishLink_Click(object sender, RoutedEventArgs e) - { - ShowDialog(UIControllerFlow.Publish); - } - - private void twoFactorTester_Click(object sender, RoutedEventArgs e) - { - var twoFactorTester = new TwoFactorInputTester(); - twoFactorTester.ShowDialog(); - } - - void ShowDialog(UIControllerFlow flow) - { - var ui = App.ServiceProvider.GetExportedValue(); - - var factory = ui.GetService(); - var d = factory.UIControllerFactory.CreateExport(); - var userControlObservable = d.Value.SelectFlow(flow); - var x = new WindowController(userControlObservable); - userControlObservable.Subscribe(_ => { }, _ => x.Close()); - x.Show(); - d.Value.Start(null); - } - } -} diff --git a/src/DesignTimeStyleHelper/Properties/AssemblyInfo.cs b/src/DesignTimeStyleHelper/Properties/AssemblyInfo.cs deleted file mode 100644 index ccdfc3b4fe..0000000000 --- a/src/DesignTimeStyleHelper/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Reflection; -using System.Resources; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Windows; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("DesignTimeStyleHelper")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DesignTimeStyleHelper")] -[assembly: AssemblyCopyright("Copyright © 2015")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -//In order to begin building localizable applications, set -//CultureYouAreCodingWith in your .csproj file -//inside a . For example, if you are using US english -//in your source files, set the to en-US. Then uncomment -//the NeutralResourceLanguage attribute below. Update the "en-US" in -//the line below to match the UICulture setting in the project file. - -//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] - - -[assembly: ThemeInfo( - ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located - //(used if a resource is not found in the page, - // or application resource dictionaries) - ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located - //(used if a resource is not found in the page, - // app, or any theme specific resource dictionaries) -)] - - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/DesignTimeStyleHelper/Properties/Resources.Designer.cs b/src/DesignTimeStyleHelper/Properties/Resources.Designer.cs deleted file mode 100644 index 7d10cee3a3..0000000000 --- a/src/DesignTimeStyleHelper/Properties/Resources.Designer.cs +++ /dev/null @@ -1,71 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.0 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace DesignTimeStyleHelper.Properties -{ - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources - { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() - { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager - { - get - { - if ((resourceMan == null)) - { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DesignTimeStyleHelper.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture - { - get - { - return resourceCulture; - } - set - { - resourceCulture = value; - } - } - } -} diff --git a/src/DesignTimeStyleHelper/Properties/Resources.resx b/src/DesignTimeStyleHelper/Properties/Resources.resx deleted file mode 100644 index af7dbebbac..0000000000 --- a/src/DesignTimeStyleHelper/Properties/Resources.resx +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/src/DesignTimeStyleHelper/Properties/Settings.Designer.cs b/src/DesignTimeStyleHelper/Properties/Settings.Designer.cs deleted file mode 100644 index a3392b1a2f..0000000000 --- a/src/DesignTimeStyleHelper/Properties/Settings.Designer.cs +++ /dev/null @@ -1,30 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.0 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace DesignTimeStyleHelper.Properties -{ - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase - { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default - { - get - { - return defaultInstance; - } - } - } -} diff --git a/src/DesignTimeStyleHelper/Properties/Settings.settings b/src/DesignTimeStyleHelper/Properties/Settings.settings deleted file mode 100644 index 033d7a5e9e..0000000000 --- a/src/DesignTimeStyleHelper/Properties/Settings.settings +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/DesignTimeStyleHelper/TwoFactorInputTester.xaml b/src/DesignTimeStyleHelper/TwoFactorInputTester.xaml deleted file mode 100644 index 5186888afc..0000000000 --- a/src/DesignTimeStyleHelper/TwoFactorInputTester.xaml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - diff --git a/src/DesignTimeStyleHelper/TwoFactorInputTester.xaml.cs b/src/DesignTimeStyleHelper/TwoFactorInputTester.xaml.cs deleted file mode 100644 index 581dc40f34..0000000000 --- a/src/DesignTimeStyleHelper/TwoFactorInputTester.xaml.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Windows; - -namespace DesignTimeStyleHelper -{ - /// - /// Interaction logic for TwoFactorInputTester.xaml - /// - public partial class TwoFactorInputTester : Window - { - public TwoFactorInputTester() - { - InitializeComponent(); - } - } -} diff --git a/src/DesignTimeStyleHelper/WindowController.xaml b/src/DesignTimeStyleHelper/WindowController.xaml deleted file mode 100644 index 3e88df1b23..0000000000 --- a/src/DesignTimeStyleHelper/WindowController.xaml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - diff --git a/src/DesignTimeStyleHelper/WindowController.xaml.cs b/src/DesignTimeStyleHelper/WindowController.xaml.cs deleted file mode 100644 index 81ae1f53ad..0000000000 --- a/src/DesignTimeStyleHelper/WindowController.xaml.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Windows; -using System.Windows.Controls; - -namespace DesignTimeStyleHelper -{ - /// - /// Interaction logic for WindowController.xaml - /// - public partial class WindowController : Window - { - IDisposable disposable; - - public WindowController(IObservable controls) - { - InitializeComponent(); - - disposable = controls.Subscribe(c => Load(c), - Close - ); - } - - protected override void OnClosed(EventArgs e) - { - disposable.Dispose(); - base.OnClosed(e); - } - - public void Load(UserControl control) - { - Container.Children.Clear(); - Container.Children.Add(control); - } - } -} diff --git a/src/DesignTimeStyleHelper/packages.config b/src/DesignTimeStyleHelper/packages.config deleted file mode 100644 index 59062b8c38..0000000000 --- a/src/DesignTimeStyleHelper/packages.config +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/GitHub.Api/ApiClientConfiguration.cs b/src/GitHub.Api/ApiClientConfiguration.cs new file mode 100644 index 0000000000..fc8cba3eb7 --- /dev/null +++ b/src/GitHub.Api/ApiClientConfiguration.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Security.Cryptography; +using System.Text; + +namespace GitHub.Api +{ + /// + /// Holds the configuration for API clients. + /// + public static partial class ApiClientConfiguration + { + /// + /// Initializes static members of the class. + /// + static ApiClientConfiguration() + { + Configure(); + } + + /// + /// Gets the application's OAUTH client ID. + /// + public static string ClientId { get; private set; } + + /// + /// Gets the application's OAUTH client secret. + /// + public static string ClientSecret { get; private set; } + + /// + /// Gets the scopes required by the application. + /// + public static IReadOnlyList RequiredScopes { get; } = new[] { "user", "repo", "gist", "write:public_key" }; + + /// + /// Gets a note that will be stored with the OAUTH token. + /// + public static string AuthorizationNote + { + get { return Info.ApplicationInfo.ApplicationDescription + " on " + GetMachineNameSafe(); } + } + + /// + /// Gets the machine fingerprint that will be registered with the OAUTH token, allowing + /// multiple authorizations to be created for a single user. + /// + public static string MachineFingerprint + { + get + { + return GetSha256Hash( + Info.ApplicationInfo.ApplicationDescription + ":" + + GetMachineIdentifier() + ":" + + GetMachineNameSafe()); + } + } + + static partial void Configure(); + + static string GetMachineIdentifier() + { + try + { + // adapted from http://stackoverflow.com/a/1561067 + var fastestValidNetworkInterface = NetworkInterface.GetAllNetworkInterfaces() + .OrderByDescending(nic => nic.Speed) + .Where(nic => nic.OperationalStatus == OperationalStatus.Up) + .Select(nic => nic.GetPhysicalAddress().ToString()) + .FirstOrDefault(address => address.Length >= 12); + + return fastestValidNetworkInterface ?? GetMachineNameSafe(); + } + catch (Exception) + { + //log.Info("Could not retrieve MAC address. Fallback to using machine name.", e); + return GetMachineNameSafe(); + } + } + + static string GetMachineNameSafe() + { + try + { + return Dns.GetHostName(); + } + catch (Exception) + { + //log.Info("Failed to retrieve host name using `DNS.GetHostName`.", e); + + try + { + return Environment.MachineName; + } + catch (Exception) + { + //log.Info("Failed to retrieve host name using `Environment.MachineName`.", ex); + return "(unknown)"; + } + } + } + + static string GetSha256Hash(string input) + { + try + { + using (var sha256 = SHA256.Create()) + { + var bytes = Encoding.UTF8.GetBytes(input); + var hash = sha256.ComputeHash(bytes); + + return string.Join("", hash.Select(b => b.ToString("x2", CultureInfo.InvariantCulture))); + } + } + catch (Exception) + { + //log.Error("IMPOSSIBLE! Generating Sha256 hash caused an exception.", e); + return null; + } + } + } +} diff --git a/src/GitHub.Api/ApiClientConfiguration_User.cs b/src/GitHub.Api/ApiClientConfiguration_User.cs new file mode 100644 index 0000000000..fdffb967e8 --- /dev/null +++ b/src/GitHub.Api/ApiClientConfiguration_User.cs @@ -0,0 +1,16 @@ +using System; + +namespace GitHub.Api +{ + static partial class ApiClientConfiguration + { + const string clientId = "YOUR CLIENT ID HERE"; + const string clientSecret = "YOUR CLIENT SECRET HERE"; + + static partial void Configure() + { + ClientId = clientId; + ClientSecret = clientSecret; + } + } +} diff --git a/src/GitHub.Api/GitHub.Api.csproj b/src/GitHub.Api/GitHub.Api.csproj index ad15b3a88e..28f1263421 100644 --- a/src/GitHub.Api/GitHub.Api.csproj +++ b/src/GitHub.Api/GitHub.Api.csproj @@ -9,41 +9,57 @@ Properties GitHub.Api GitHub.Api - v4.5 + 7.3 + v4.6.1 512 - Internal + ..\common\GitHubVS.ruleset + true + true true full false - bin\Debug\ DEBUG;TRACE prompt 4 + false + bin\Debug\ + + + true + full + false + CODE_ANALYSIS;DEBUG;TRACE + prompt + 4 true - ..\common\GitHubVS.ruleset - true - true + bin\Debug\ pdbonly true - bin\Release\ TRACE prompt 4 true - true - true - ..\common\GitHubVS.ruleset - - - ..\..\script\Key.snk - true - false + bin\Release\ + + + ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + + + ..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.dll + + + ..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll + + + ..\..\packages\Serilog.2.5.0\lib\net46\Serilog.dll + True + @@ -55,10 +71,22 @@ - - - Key.snk - + + + ApiClientConfiguration_User.cs + + + + + + + + + + + + + Properties\SolutionInfo.cs @@ -85,6 +113,13 @@ {6afe2e2d-6db0-4430-a2ea-f5f5388d2f78} GitHub.Extensions + + {8d73575a-a89f-47cc-b153-b47dd06837f0} + GitHub.Logging + + + + + $(VisualStudioVersion) + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + true + + + + + Debug + AnyCPU + 2.0 + {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + {7F5ED78B-74A3-4406-A299-70CFB5885B8B} + Library + Properties + GitHub.InlineReviews + GitHub.InlineReviews + 7.3 + v4.6.1 + true + true + true + true + true + true + ..\common\GitHubVS.ruleset + true + true + False + False + + + true + full + false + TRACE;DEBUG + prompt + 4 + false + bin\Debug\ + + + true + full + false + TRACE;DEBUG;CODE_ANALYSIS + prompt + 4 + true + bin\Debug\ + + + pdbonly + true + TRACE + prompt + 4 + true + bin\Release\ + + + + + Properties\SolutionInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PullRequestFileMarginView.xaml + + + GlyphMarginGrid.xaml + + + InlineCommentPeekView.xaml + + + + + + + AddInlineCommentGlyph.xaml + + + ShowInlineCommentGlyph.xaml + + + + + + + + + + CommentThreadView.xaml + + + CommentView.xaml + + + PullRequestStatusView.xaml + + + + + + Designer + + + Designer + + + + + {08dd4305-7787-4823-a53f-4d0f725a07f3} + Octokit + + + {1CE2D235-8072-4649-BA5A-CFB1AF8776E0} + ReactiveUI_Net45 + + + {252ce1c2-027a-4445-a3c2-e4d6c80a935a} + Splat-Net45 + + + {b389adaf-62cc-486e-85b4-2d8b078df763} + GitHub.Api + + + {1A1DA411-8D1F-4578-80A6-04576BEA2DC5} + GitHub.App + + + {e4ed0537-d1d9-44b6-9212-3096d7c3f7a1} + GitHub.Exports.Reactive + + + {9aea02db-02b5-409c-b0ca-115d05331a6b} + GitHub.Exports + + + {6559E128-8B40-49A5-85A8-05565ED0C7E3} + GitHub.Extensions.Reactive + + + {6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78} + GitHub.Extensions + + + {8d73575a-a89f-47cc-b153-b47dd06837f0} + GitHub.Logging + + + {2d3d2834-33be-45ca-b3cc-12f853557d7b} + GitHub.Services.Vssdk + + + {158b05e8-fdbc-4d71-b871-c96e28d5adf5} + GitHub.UI.Reactive + + + {346384dd-2445-4a28-af22-b45f3957bd89} + GitHub.UI + + + {d1dfbb0c-b570-4302-8f1e-2e3a19c41961} + GitHub.VisualStudio.UI + + + + + False + + + False + + + False + + + False + + + ..\..\packages\LibGit2Sharp.0.23.1\lib\net40\LibGit2Sharp.dll + True + + + ..\..\packages\Markdig.Signed.0.13.0\lib\net40\Markdig.dll + True + + + ..\..\packages\Markdig.Wpf.Signed.0.2.1\lib\net452\Markdig.Wpf.dll + True + + + + + False + + + ..\..\packages\Microsoft.VisualStudio.ComponentModelHost.14.0.25424\lib\net45\Microsoft.VisualStudio.ComponentModelHost.dll + True + + + ..\..\packages\Microsoft.VisualStudio.CoreUtility.14.3.25407\lib\net45\Microsoft.VisualStudio.CoreUtility.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Editor.14.3.25407\lib\net45\Microsoft.VisualStudio.Editor.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Imaging.14.3.25407\lib\net45\Microsoft.VisualStudio.Imaging.dll + True + + + True + ..\..\packages\Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime.14.3.25407\lib\Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Language.Intellisense.14.3.25407\lib\net45\Microsoft.VisualStudio.Language.Intellisense.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 + + + ..\..\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.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 + + + True + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.10.0.10.0.30319\lib\Microsoft.VisualStudio.Shell.Interop.10.0.dll + True + + + True + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.11.0.11.0.61030\lib\Microsoft.VisualStudio.Shell.Interop.11.0.dll + True + + + True + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.12.0.12.0.30110\lib\Microsoft.VisualStudio.Shell.Interop.12.0.dll + True + + + True + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.14.0.DesignTime.14.3.25407\lib\Microsoft.VisualStudio.Shell.Interop.14.0.DesignTime.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.8.0.8.0.50727\lib\Microsoft.VisualStudio.Shell.Interop.8.0.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.9.0.9.0.30729\lib\Microsoft.VisualStudio.Shell.Interop.9.0.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Text.Data.14.3.25407\lib\net45\Microsoft.VisualStudio.Text.Data.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Text.Logic.14.3.25407\lib\net45\Microsoft.VisualStudio.Text.Logic.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Text.UI.14.3.25407\lib\net45\Microsoft.VisualStudio.Text.UI.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Text.UI.Wpf.14.3.25407\lib\net45\Microsoft.VisualStudio.Text.UI.Wpf.dll + True + + + ..\..\packages\Microsoft.VisualStudio.TextManager.Interop.7.10.6070\lib\Microsoft.VisualStudio.TextManager.Interop.dll + True + + + ..\..\packages\Microsoft.VisualStudio.TextManager.Interop.8.0.8.0.50727\lib\Microsoft.VisualStudio.TextManager.Interop.8.0.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Threading.14.1.111\lib\net45\Microsoft.VisualStudio.Threading.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Utilities.14.3.25407\lib\net45\Microsoft.VisualStudio.Utilities.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Validation.14.1.111\lib\net45\Microsoft.VisualStudio.Validation.dll + True + + + ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + + + ..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.dll + + + ..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll + + + + + ..\..\packages\Serilog.2.5.0\lib\net46\Serilog.dll + True + + + False + + + + + + + + + + ..\..\packages\Rx-Core.2.2.5-custom\lib\net45\System.Reactive.Core.dll + True + + + ..\..\packages\Rx-Interfaces.2.2.5-custom\lib\net45\System.Reactive.Interfaces.dll + True + + + ..\..\packages\Rx-Linq.2.2.5-custom\lib\net45\System.Reactive.Linq.dll + True + + + ..\..\packages\Rx-PlatformServices.2.2.5-custom\lib\net45\System.Reactive.PlatformServices.dll + True + + + ..\..\packages\System.ValueTuple.4.5.0\lib\net461\System.ValueTuple.dll + + + + + + + + + + Menus.ctmenu + Designer + + + + + true + VSPackage + Designer + + + + + MSBuild:Compile + Designer + true + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + Designer + MSBuild:Compile + + + + + + + + PreserveNewest + + + + + + + + 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}. + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.InlineReviews/Glyph/GlyphData.cs b/src/GitHub.InlineReviews/Glyph/GlyphData.cs new file mode 100644 index 0000000000..8364b320e9 --- /dev/null +++ b/src/GitHub.InlineReviews/Glyph/GlyphData.cs @@ -0,0 +1,46 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using Microsoft.VisualStudio.Text; + +namespace GitHub.InlineReviews.Glyph.Implementation +{ + /// + /// Information about the position of a glyph. + /// + /// The type of glyph tag we're dealing with. + internal class GlyphData + { + double deltaY; + + public GlyphData(SnapshotSpan visualSpan, TGlyphTag tag, UIElement element) + { + VisualSpan = visualSpan; + GlyphType = tag.GetType(); + Glyph = element; + + deltaY = Canvas.GetTop(element); + if (double.IsNaN(deltaY)) + { + deltaY = 0.0; + } + } + + public void SetSnapshot(ITextSnapshot snapshot) + { + VisualSpan = VisualSpan.Value.TranslateTo(snapshot, SpanTrackingMode.EdgeInclusive); + } + + public void SetTop(double top) + { + Canvas.SetTop(Glyph, top + deltaY); + } + + public UIElement Glyph { get; } + + public Type GlyphType { get; } + + public SnapshotSpan? VisualSpan { get; private set; } + } +} + diff --git a/src/GitHub.InlineReviews/Glyph/GlyphMargin.cs b/src/GitHub.InlineReviews/Glyph/GlyphMargin.cs new file mode 100644 index 0000000000..7f9df1c80b --- /dev/null +++ b/src/GitHub.InlineReviews/Glyph/GlyphMargin.cs @@ -0,0 +1,154 @@ +using System; +using System.Reactive.Linq; +using System.Windows.Media; +using System.Windows.Controls; +using System.Collections.Generic; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Tagging; +using Microsoft.VisualStudio.Text.Formatting; +using Microsoft.VisualStudio.Text.Classification; +using Microsoft.VisualStudio.Text.Editor; +using GitHub.InlineReviews.Glyph.Implementation; +using ReactiveUI; + +namespace GitHub.InlineReviews.Glyph +{ + /// + /// Responsibe for updating the margin when tags change. + /// + /// The type of glyph tag we're managing. + public sealed class GlyphMargin : IDisposable where TGlyphTag : ITag + { + readonly IWpfTextView textView; + readonly Grid marginGrid; + readonly IViewTagAggregatorFactoryService tagAggregatorFactory; + readonly GlyphMarginVisualManager visualManager; + + IDisposable visibleSubscription; + bool refreshAllGlyphs; + ITagAggregator tagAggregator; + bool disposed; + + public GlyphMargin( + IWpfTextView textView, + IGlyphFactory glyphFactory, + Grid marginGrid, + IViewTagAggregatorFactoryService tagAggregatorFactory, + IEditorFormatMap editorFormatMap, + string marginPropertiesName) + { + this.textView = textView; + this.marginGrid = marginGrid; + this.tagAggregatorFactory = tagAggregatorFactory; + visualManager = new GlyphMarginVisualManager(textView, glyphFactory, marginGrid, editorFormatMap, marginPropertiesName); + + // Initialize when first visible + visibleSubscription = marginGrid.WhenAnyValue(x => x.IsVisible).Distinct().Where(x => x).Subscribe(_ => Initialize()); + } + + public void Dispose() + { + if (!disposed) + { + disposed = true; + + textView.LayoutChanged -= OnLayoutChanged; + textView.ZoomLevelChanged -= OnZoomLevelChanged; + + tagAggregator?.Dispose(); + tagAggregator = null; + + visibleSubscription?.Dispose(); + visibleSubscription = null; + } + } + + void Initialize() + { + tagAggregator = tagAggregatorFactory.CreateTagAggregator(textView); + tagAggregator.BatchedTagsChanged += OnBatchedTagsChanged; + textView.LayoutChanged += OnLayoutChanged; + textView.ZoomLevelChanged += OnZoomLevelChanged; + + if (textView.InLayout) + { + refreshAllGlyphs = true; + } + else + { + foreach (var line in textView.TextViewLines) + { + RefreshGlyphsOver(line); + } + } + + marginGrid.LayoutTransform = new ScaleTransform(textView.ZoomLevel / 100.0, textView.ZoomLevel / 100.0); + marginGrid.LayoutTransform.Freeze(); + } + + void OnBatchedTagsChanged(object sender, BatchedTagsChangedEventArgs e) + { + if (!textView.IsClosed) + { + var list = new List(); + foreach (var span in e.Spans) + { + list.AddRange(span.GetSpans(textView.TextSnapshot)); + } + + if (list.Count > 0) + { + var span = list[0]; + int start = span.Start; + int end = span.End; + for (int i = 1; i < list.Count; i++) + { + span = list[i]; + start = Math.Min(start, span.Start); + end = Math.Max(end, span.End); + } + + var rangeSpan = new SnapshotSpan(textView.TextSnapshot, start, end - start); + visualManager.RemoveGlyphsByVisualSpan(rangeSpan); + foreach (var line in textView.TextViewLines.GetTextViewLinesIntersectingSpan(rangeSpan)) + { + RefreshGlyphsOver(line); + } + } + } + } + + void OnLayoutChanged(object sender, TextViewLayoutChangedEventArgs e) + { + visualManager.SetSnapshotAndUpdate(textView.TextSnapshot, e.NewOrReformattedLines, e.VerticalTranslation ? (IList)textView.TextViewLines : e.TranslatedLines); + + var lines = refreshAllGlyphs ? (IList)textView.TextViewLines : e.NewOrReformattedLines; + foreach (var line in lines) + { + visualManager.RemoveGlyphsByVisualSpan(line.Extent); + RefreshGlyphsOver(line); + } + + refreshAllGlyphs = false; + } + + void OnZoomLevelChanged(object sender, ZoomLevelChangedEventArgs e) + { + refreshAllGlyphs = true; + marginGrid.LayoutTransform = e.ZoomTransform; + } + + void RefreshGlyphsOver(ITextViewLine textViewLine) + { + foreach (IMappingTagSpan span in tagAggregator.GetTags(textViewLine.ExtentAsMappingSpan)) + { + NormalizedSnapshotSpanCollection spans; + if (span.Span.Start.GetPoint(textView.VisualSnapshot.TextBuffer, PositionAffinity.Predecessor).HasValue && + ((spans = span.Span.GetSpans(textView.TextSnapshot)).Count > 0)) + { + visualManager.AddGlyph(span.Tag, spans[0]); + } + } + } + } +} diff --git a/src/GitHub.InlineReviews/Glyph/GlyphMarginVisualManager.cs b/src/GitHub.InlineReviews/Glyph/GlyphMarginVisualManager.cs new file mode 100644 index 0000000000..bc5444919b --- /dev/null +++ b/src/GitHub.InlineReviews/Glyph/GlyphMarginVisualManager.cs @@ -0,0 +1,199 @@ +using System; +using System.Windows; +using System.Windows.Media; +using System.Windows.Controls; +using System.Collections.Generic; +using Microsoft.VisualStudio.PlatformUI; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Tagging; +using Microsoft.VisualStudio.Text.Formatting; +using Microsoft.VisualStudio.Text.Classification; + +namespace GitHub.InlineReviews.Glyph.Implementation +{ + /// + /// Manage the MarginVisual element. + /// + /// The type of tag we're managing. + internal class GlyphMarginVisualManager where TGlyphTag : ITag + { + readonly IEditorFormatMap editorFormatMap; + readonly IGlyphFactory glyphFactory; + readonly Grid glyphMarginGrid; + readonly string marginPropertiesName; + readonly IWpfTextView textView; + readonly Dictionary visuals; + + Dictionary> glyphs; + + public GlyphMarginVisualManager(IWpfTextView textView, IGlyphFactory glyphFactory, Grid glyphMarginGrid, + IEditorFormatMap editorFormatMap, string marginPropertiesName) + { + this.textView = textView; + this.marginPropertiesName = marginPropertiesName; + this.editorFormatMap = editorFormatMap; + this.editorFormatMap.FormatMappingChanged += OnFormatMappingChanged; + this.textView.Closed += new EventHandler(OnTextViewClosed); + this.glyphFactory = glyphFactory; + this.glyphMarginGrid = glyphMarginGrid; + UpdateBackgroundColor(); + + glyphs = new Dictionary>(); + visuals = new Dictionary(); + + foreach (Type type in glyphFactory.GetTagTypes()) + { + if (!visuals.ContainsKey(type)) + { + var element = new Canvas(); + element.ClipToBounds = true; + glyphMarginGrid.Children.Add(element); + visuals[type] = element; + } + } + } + + public void AddGlyph(TGlyphTag tag, SnapshotSpan span) + { + var textViewLines = textView.TextViewLines; + var glyphType = tag.GetType(); + if (textView.TextViewLines.IntersectsBufferSpan(span)) + { + var startingLine = GetStartingLine(textViewLines, span) as IWpfTextViewLine; + if (startingLine != null) + { + var element = (FrameworkElement)glyphFactory.GenerateGlyph(startingLine, tag); + if (element != null) + { + var data = new GlyphData(span, tag, element); + element.Width = glyphMarginGrid.Width; + + // draw where text is + element.Height = startingLine.TextHeight + 1; // HACK: +1 to fill gaps + data.SetTop(startingLine.TextTop - textView.ViewportTop); + + glyphs[element] = data; + visuals[glyphType].Children.Add(element); + } + } + } + } + + public void RemoveGlyphsByVisualSpan(SnapshotSpan span) + { + var list = new List(); + foreach (var pair in glyphs) + { + var data = pair.Value; + if (data.VisualSpan.HasValue && span.IntersectsWith(data.VisualSpan.Value)) + { + list.Add(pair.Key); + visuals[data.GlyphType].Children.Remove(data.Glyph); + } + } + + foreach (var element in list) + { + glyphs.Remove(element); + } + } + + public void SetSnapshotAndUpdate(ITextSnapshot snapshot, IList newOrReformattedLines, IList translatedLines) + { + if (glyphs.Count > 0) + { + var dictionary = new Dictionary>(glyphs.Count); + foreach (var pair in glyphs) + { + var data = pair.Value; + if (!data.VisualSpan.HasValue) + { + dictionary[pair.Key] = data; + continue; + } + + data.SetSnapshot(snapshot); + SnapshotSpan bufferSpan = data.VisualSpan.Value; + if (!textView.TextViewLines.IntersectsBufferSpan(bufferSpan) || GetStartingLine(newOrReformattedLines, bufferSpan) != null) + { + visuals[data.GlyphType].Children.Remove(data.Glyph); + continue; + } + + dictionary[data.Glyph] = data; + var startingLine = GetStartingLine(translatedLines, bufferSpan); + if (startingLine != null) + { + data.SetTop(startingLine.TextTop - textView.ViewportTop); + } + } + + glyphs = dictionary; + } + } + + static ITextViewLine GetStartingLine(IList lines, Span span) + { + if (lines.Count > 0) + { + int num = 0; + int count = lines.Count; + while (num < count) + { + int middle = (num + count) / 2; + var middleLine = lines[middle]; + if (span.Start < middleLine.Start) + { + count = middle; + } + else + { + if (span.Start >= middleLine.EndIncludingLineBreak) + { + num = middle + 1; + continue; + } + + return middleLine; + } + } + + var line = lines[lines.Count - 1]; + if (line.EndIncludingLineBreak == line.Snapshot.Length && span.Start == line.EndIncludingLineBreak) + { + return line; + } + } + + return null; + } + + void OnFormatMappingChanged(object sender, FormatItemsEventArgs e) + { + if (e.ChangedItems.Contains(marginPropertiesName)) + { + UpdateBackgroundColor(); + } + } + + void OnTextViewClosed(object sender, EventArgs e) + { + editorFormatMap.FormatMappingChanged -= OnFormatMappingChanged; + } + + void UpdateBackgroundColor() + { + // set background color for children + var properties = editorFormatMap.GetProperties(marginPropertiesName); + if (properties.Contains("BackgroundColor")) + { + var backgroundColor = (Color)properties["BackgroundColor"]; + ImageThemingUtilities.SetImageBackgroundColor(glyphMarginGrid, backgroundColor); + } + } + + public FrameworkElement MarginVisual => glyphMarginGrid; + } +} + diff --git a/src/GitHub.InlineReviews/Glyph/IGlyphFactory.cs b/src/GitHub.InlineReviews/Glyph/IGlyphFactory.cs new file mode 100644 index 0000000000..6ec57355c4 --- /dev/null +++ b/src/GitHub.InlineReviews/Glyph/IGlyphFactory.cs @@ -0,0 +1,29 @@ +using System; +using System.Windows; +using System.Collections.Generic; +using Microsoft.VisualStudio.Text.Tagging; +using Microsoft.VisualStudio.Text.Formatting; + +namespace GitHub.InlineReviews.Glyph +{ + /// + /// A factory for a type of tag (or multiple subtypes). + /// + /// + public interface IGlyphFactory where TGlyphTag : ITag + { + /// + /// Create a glyph element for a particular line and tag. + /// + /// The line. + /// The tag. + /// + UIElement GenerateGlyph(IWpfTextViewLine line, TGlyphTag tag); + + /// + /// A list of tag subtypes this is a factory for. + /// + /// + IEnumerable GetTagTypes(); + } +} diff --git a/src/GitHub.InlineReviews/InlineReviewsPackage.cs b/src/GitHub.InlineReviews/InlineReviewsPackage.cs new file mode 100644 index 0000000000..2531cc6788 --- /dev/null +++ b/src/GitHub.InlineReviews/InlineReviewsPackage.cs @@ -0,0 +1,61 @@ +using System; +using System.ComponentModel.Design; +using System.Runtime.InteropServices; +using System.Threading; +using GitHub.Exports; +using GitHub.Logging; +using GitHub.Commands; +using GitHub.Services.Vssdk.Commands; +using GitHub.VisualStudio; +using Microsoft.VisualStudio.ComponentModelHost; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Threading; +using Serilog; +using Task = System.Threading.Tasks.Task; + +namespace GitHub.InlineReviews +{ + [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] + [Guid(Guids.InlineReviewsPackageId)] + [ProvideAutoLoad(Guids.UIContext_Git, PackageAutoLoadFlags.BackgroundLoad)] + [ProvideMenuResource("Menus.ctmenu", 1)] + public class InlineReviewsPackage : AsyncPackage + { + static readonly ILogger log = LogManager.ForContext(); + + protected override async Task InitializeAsync( + CancellationToken cancellationToken, + IProgress progress) + { + var menuService = (IMenuCommandService)(await GetServiceAsync(typeof(IMenuCommandService))); + var componentModel = (IComponentModel)(await GetServiceAsync(typeof(SComponentModel))); + var exports = componentModel.DefaultExportProvider; + + // Avoid delays when there is ongoing UI activity. + // See: https://github.com/github/VisualStudio/issues/1537 + await JoinableTaskFactory.RunAsync(VsTaskRunContext.UIThreadNormalPriority, InitializeMenus); + } + + async Task InitializeMenus() + { + if (!ExportForVisualStudioProcessAttribute.IsVisualStudioProcess()) + { + log.Warning("Don't initialize menus for non-Visual Studio process"); + return; + } + + var componentModel = (IComponentModel)(await GetServiceAsync(typeof(SComponentModel))); + var exports = componentModel.DefaultExportProvider; + var commands = new IVsCommandBase[] + { + exports.GetExportedValue(), + exports.GetExportedValue(), + exports.GetExportedValue() + }; + + await JoinableTaskFactory.SwitchToMainThreadAsync(); + var menuService = (IMenuCommandService)(await GetServiceAsync(typeof(IMenuCommandService))); + menuService.AddCommands(commands); + } + } +} diff --git a/src/GitHub.InlineReviews/InlineReviewsPackage.vsct b/src/GitHub.InlineReviews/InlineReviewsPackage.vsct new file mode 100644 index 0000000000..d9e99ccb84 --- /dev/null +++ b/src/GitHub.InlineReviews/InlineReviewsPackage.vsct @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GitHub.InlineReviews/Margins/InlineCommentMargin.cs b/src/GitHub.InlineReviews/Margins/InlineCommentMargin.cs new file mode 100644 index 0000000000..6736d3f855 --- /dev/null +++ b/src/GitHub.InlineReviews/Margins/InlineCommentMargin.cs @@ -0,0 +1,141 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Threading.Tasks; +using GitHub.Models; +using GitHub.Services; +using GitHub.Extensions; +using GitHub.InlineReviews.Tags; +using GitHub.InlineReviews.Views; +using GitHub.InlineReviews.Glyph; +using GitHub.InlineReviews.Services; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Tagging; +using Microsoft.VisualStudio.Text.Classification; +using ReactiveUI; + +namespace GitHub.InlineReviews.Margins +{ + public sealed class InlineCommentMargin : IWpfTextViewMargin + { + public const string MarginName = "InlineComment"; + const string MarginPropertiesName = "Indicator Margin"; // Same background color as Glyph margin + + readonly IWpfTextView textView; + readonly IPullRequestSessionManager sessionManager; + readonly Grid marginGrid; + + GlyphMargin glyphMargin; + IDisposable currentSessionSubscription; + IDisposable visibleSubscription; + bool hasChanges; + bool hasInfo; + + public InlineCommentMargin( + IWpfTextViewHost wpfTextViewHost, + IInlineCommentPeekService peekService, + IEditorFormatMapService editorFormatMapService, + IViewTagAggregatorFactoryService tagAggregatorFactory, + Lazy sessionManager) + { + textView = wpfTextViewHost.TextView; + this.sessionManager = sessionManager.Value; + + // Default to not show comment margin + textView.Options.SetOptionValue(InlineCommentTextViewOptions.MarginEnabledId, false); + + marginGrid = new GlyphMarginGrid { Width = 17.0 }; + var glyphFactory = new InlineCommentGlyphFactory(peekService, textView); + var editorFormatMap = editorFormatMapService.GetEditorFormatMap(textView); + + glyphMargin = new GlyphMargin(textView, glyphFactory, marginGrid, tagAggregatorFactory, + editorFormatMap, MarginPropertiesName); + + if (IsDiffView()) + { + TrackCommentGlyph(wpfTextViewHost, marginGrid); + } + + currentSessionSubscription = this.sessionManager.WhenAnyValue(x => x.CurrentSession) + .Subscribe(x => RefreshCurrentSession().Forget()); + + visibleSubscription = marginGrid.WhenAnyValue(x => x.IsVisible) + .Subscribe(x => textView.Options.SetOptionValue(InlineCommentTextViewOptions.MarginVisibleId, x)); + + textView.Options.OptionChanged += (s, e) => RefreshMarginVisibility(); + } + + async Task RefreshCurrentSession() + { + var sessionFile = await FindSessionFile(); + hasChanges = sessionFile?.Diff != null && sessionFile.Diff.Count > 0; + + await Task.Yield(); // HACK: Give diff view a chance to initialize. + var info = sessionManager.GetTextBufferInfo(textView.TextBuffer); + hasInfo = info != null; + + RefreshMarginVisibility(); + } + + public ITextViewMargin GetTextViewMargin(string name) + { + return (name == MarginName) ? this : null; + } + + public void Dispose() + { + visibleSubscription?.Dispose(); + visibleSubscription = null; + + currentSessionSubscription?.Dispose(); + currentSessionSubscription = null; + + glyphMargin?.Dispose(); + glyphMargin = null; + } + + public FrameworkElement VisualElement => marginGrid; + + public double MarginSize => marginGrid.Width; + + public bool Enabled => IsMarginVisible(); + + async Task FindSessionFile() + { + await sessionManager.EnsureInitialized(); + + var session = sessionManager.CurrentSession; + if (session == null) + { + return null; + } + + var relativePath = sessionManager.GetRelativePath(textView.TextBuffer); + if (relativePath == null) + { + return null; + } + + return await session.GetFile(relativePath); + } + + bool IsDiffView() => textView.Roles.Contains("DIFF"); + + void TrackCommentGlyph(IWpfTextViewHost host, UIElement marginElement) + { + var router = new MouseEnterAndLeaveEventRouter(); + router.Add(host.HostControl, marginElement); + } + + void RefreshMarginVisibility() + { + marginGrid.Visibility = IsMarginVisible() ? Visibility.Visible : Visibility.Collapsed; + } + + bool IsMarginVisible() + { + var enabled = textView.Options.GetOptionValue(InlineCommentTextViewOptions.MarginEnabledId); + return hasInfo || (enabled && hasChanges); + } + } +} diff --git a/src/GitHub.InlineReviews/Margins/InlineCommentMarginEnabled.cs b/src/GitHub.InlineReviews/Margins/InlineCommentMarginEnabled.cs new file mode 100644 index 0000000000..3249371eff --- /dev/null +++ b/src/GitHub.InlineReviews/Margins/InlineCommentMarginEnabled.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Text.Editor; + +namespace GitHub.InlineReviews.Margins +{ + [Export(typeof(EditorOptionDefinition))] + public class InlineCommentMarginEnabled : ViewOptionDefinition + { + public override bool Default => false; + + public override EditorOptionKey Key => InlineCommentTextViewOptions.MarginEnabledId; + } +} diff --git a/src/GitHub.InlineReviews/Margins/InlineCommentMarginProvider.cs b/src/GitHub.InlineReviews/Margins/InlineCommentMarginProvider.cs new file mode 100644 index 0000000000..36cbda2c6f --- /dev/null +++ b/src/GitHub.InlineReviews/Margins/InlineCommentMarginProvider.cs @@ -0,0 +1,59 @@ +using System; +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Utilities; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Tagging; +using Microsoft.VisualStudio.Text.Classification; +using GitHub.Services; +using GitHub.VisualStudio; +using GitHub.InlineReviews.Services; + +namespace GitHub.InlineReviews.Margins +{ + [Export(typeof(IWpfTextViewMarginProvider))] + [Name(InlineCommentMargin.MarginName)] + [Order(After = PredefinedMarginNames.Glyph)] + [MarginContainer(PredefinedMarginNames.Left)] + [ContentType("text")] + [TextViewRole(PredefinedTextViewRoles.Interactive)] + internal sealed class InlineCommentMarginProvider : IWpfTextViewMarginProvider + { + readonly Lazy editorFormatMapService; + readonly Lazy tagAggregatorFactory; + readonly Lazy peekService; + readonly Lazy sessionManager; + readonly UIContext uiContext; + + [ImportingConstructor] + public InlineCommentMarginProvider( + Lazy sessionManager, + Lazy editorFormatMapService, + Lazy tagAggregatorFactory, + Lazy peekService) + { + this.sessionManager = sessionManager; + this.editorFormatMapService = editorFormatMapService; + this.tagAggregatorFactory = tagAggregatorFactory; + this.peekService = peekService; + + uiContext = UIContext.FromUIContextGuid(new Guid(Guids.UIContext_Git)); + } + + public IWpfTextViewMargin CreateMargin(IWpfTextViewHost wpfTextViewHost, IWpfTextViewMargin parent) + { + if (!uiContext.IsActive) + { + // Only create margin when in the context of a Git repository + return null; + } + + return new InlineCommentMargin( + wpfTextViewHost, + peekService.Value, + editorFormatMapService.Value, + tagAggregatorFactory.Value, + sessionManager); + } + } +} diff --git a/src/GitHub.InlineReviews/Margins/InlineCommentMarginVisible.cs b/src/GitHub.InlineReviews/Margins/InlineCommentMarginVisible.cs new file mode 100644 index 0000000000..967a9654f8 --- /dev/null +++ b/src/GitHub.InlineReviews/Margins/InlineCommentMarginVisible.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Text.Editor; + +namespace GitHub.InlineReviews.Margins +{ + [Export(typeof(EditorOptionDefinition))] + public class InlineCommentMarginVisible : ViewOptionDefinition + { + public override bool Default => false; + + public override EditorOptionKey Key => InlineCommentTextViewOptions.MarginVisibleId; + } +} diff --git a/src/GitHub.InlineReviews/Margins/InlineCommentTextViewOptions.cs b/src/GitHub.InlineReviews/Margins/InlineCommentTextViewOptions.cs new file mode 100644 index 0000000000..f2dc24da51 --- /dev/null +++ b/src/GitHub.InlineReviews/Margins/InlineCommentTextViewOptions.cs @@ -0,0 +1,11 @@ +using Microsoft.VisualStudio.Text.Editor; + +namespace GitHub.InlineReviews.Margins +{ + public static class InlineCommentTextViewOptions + { + public static EditorOptionKey MarginVisibleId = new EditorOptionKey("TextViewHost/InlineCommentMarginVisible"); + + public static EditorOptionKey MarginEnabledId = new EditorOptionKey("TextViewHost/InlineCommentMarginEnabled"); + } +} diff --git a/src/GitHub.InlineReviews/Margins/PullRequestFileMargin.cs b/src/GitHub.InlineReviews/Margins/PullRequestFileMargin.cs new file mode 100644 index 0000000000..c46311bc0a --- /dev/null +++ b/src/GitHub.InlineReviews/Margins/PullRequestFileMargin.cs @@ -0,0 +1,122 @@ +using System; +using System.IO; +using System.Windows; +using System.Threading.Tasks; +using System.Reactive.Linq; +using GitHub.Models; +using GitHub.Commands; +using GitHub.Services; +using GitHub.Extensions; +using GitHub.InlineReviews.Views; +using GitHub.InlineReviews.ViewModels; +using Microsoft.VisualStudio.Text.Editor; +using ReactiveUI; +using Task = System.Threading.Tasks.Task; + +namespace GitHub.InlineReviews.Margins +{ + /// + /// This margin appears on solution files that have a corresponding PR file (file with changes in current PR). + /// + internal class PullRequestFileMargin : IWpfTextViewMargin + { + public const string MarginName = "PullRequestFileMargin"; + + readonly IWpfTextView textView; + readonly PullRequestFileMarginViewModel viewModel; + readonly PullRequestFileMarginView visualElement; + readonly IPullRequestSessionManager sessionManager; + + bool isDisposed; + + IDisposable currentSessionSubscription; + IDisposable optionChangedSubscription; + IDisposable visibilitySubscription; + + public PullRequestFileMargin( + IWpfTextView textView, + IToggleInlineCommentMarginCommand toggleInlineCommentMarginCommand, + IGoToSolutionOrPullRequestFileCommand goToSolutionOrPullRequestFileCommand, + IPullRequestSessionManager sessionManager, + Lazy usageTracker) + { + this.textView = textView; + this.sessionManager = sessionManager; + + viewModel = new PullRequestFileMarginViewModel(toggleInlineCommentMarginCommand, goToSolutionOrPullRequestFileCommand, usageTracker); + visualElement = new PullRequestFileMarginView { DataContext = viewModel, ClipToBounds = true }; + + visibilitySubscription = viewModel.WhenAnyValue(x => x.Enabled).Subscribe(enabled => + { + visualElement.Visibility = enabled ? Visibility.Visible : Visibility.Collapsed; + }); + + optionChangedSubscription = Observable.FromEventPattern(textView.Options, nameof(textView.Options.OptionChanged)).Subscribe(_ => + { + viewModel.MarginEnabled = textView.Options.GetOptionValue(InlineCommentTextViewOptions.MarginEnabledId); + }); + + currentSessionSubscription = sessionManager.WhenAnyValue(x => x.CurrentSession) + .Subscribe(x => RefreshCurrentSession().Forget()); + } + + public void Dispose() + { + if (!isDisposed) + { + GC.SuppressFinalize(this); + isDisposed = true; + + currentSessionSubscription.Dispose(); + optionChangedSubscription.Dispose(); + visibilitySubscription.Dispose(); + } + } + + async Task RefreshCurrentSession() + { + var sessionFile = await FindSessionFile(); + if (sessionFile != null) + { + viewModel.FileName = Path.GetFileName(sessionFile.RelativePath); + viewModel.CommentsInFile = sessionFile.InlineCommentThreads?.Count ?? -1; + viewModel.Enabled = sessionFile.Diff.Count > 0; + } + else + { + viewModel.CommentsInFile = 0; + viewModel.Enabled = false; + } + } + + async Task FindSessionFile() + { + await sessionManager.EnsureInitialized(); + + var session = sessionManager.CurrentSession; + if (session == null) + { + return null; + } + + var relativePath = sessionManager.GetRelativePath(textView.TextBuffer); + if (relativePath == null) + { + return null; + } + + return await session.GetFile(relativePath); + } + + public FrameworkElement VisualElement => visualElement; + + public double MarginSize => visualElement.ActualHeight; + + public bool Enabled => viewModel.Enabled; + + public ITextViewMargin GetTextViewMargin(string marginName) + { + return string.Equals(marginName, MarginName, StringComparison.OrdinalIgnoreCase) ? this : null; + } + } +} diff --git a/src/GitHub.InlineReviews/Margins/PullRequestFileMarginProvider.cs b/src/GitHub.InlineReviews/Margins/PullRequestFileMarginProvider.cs new file mode 100644 index 0000000000..7dfafdfbc5 --- /dev/null +++ b/src/GitHub.InlineReviews/Margins/PullRequestFileMarginProvider.cs @@ -0,0 +1,86 @@ +using System; +using System.ComponentModel.Composition; +using GitHub.Commands; +using GitHub.Services; +using GitHub.Settings; +using GitHub.VisualStudio; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Utilities; +using Microsoft.VisualStudio.Text.Editor; + +namespace GitHub.InlineReviews.Margins +{ + /// + /// Export a , which returns an instance of the margin for the editor to use. + /// + [Export(typeof(IWpfTextViewMarginProvider))] + [Name(PullRequestFileMargin.MarginName)] + [Order(After = PredefinedMarginNames.ZoomControl)] + [MarginContainer(PredefinedMarginNames.BottomControl)] // Set the container to the bottom of the editor window + [ContentType("text")] // Show this margin for all text-based types + [TextViewRole(PredefinedTextViewRoles.Editable)] + internal sealed class PullRequestFileMarginProvider : IWpfTextViewMarginProvider + { + readonly Lazy sessionManager; + readonly Lazy enableInlineCommentsCommand; + readonly Lazy goToSolutionOrPullRequestFileCommand; + readonly Lazy packageSettings; + readonly Lazy usageTracker; + readonly UIContext uiContext; + + [ImportingConstructor] + public PullRequestFileMarginProvider( + Lazy enableInlineCommentsCommand, + Lazy goToSolutionOrPullRequestFileCommand, + Lazy sessionManager, + Lazy packageSettings, + Lazy usageTracker) + { + this.enableInlineCommentsCommand = enableInlineCommentsCommand; + this.goToSolutionOrPullRequestFileCommand = goToSolutionOrPullRequestFileCommand; + this.sessionManager = sessionManager; + this.packageSettings = packageSettings; + this.usageTracker = usageTracker; + + uiContext = UIContext.FromUIContextGuid(new Guid(Guids.UIContext_Git)); + } + + /// + /// Creates an for the given . + /// + /// The for which to create the . + /// The margin that will contain the newly-created margin. + /// The . + /// The value may be null if this does not participate for this context. + /// + public IWpfTextViewMargin CreateMargin(IWpfTextViewHost wpfTextViewHost, IWpfTextViewMargin marginContainer) + { + if (!uiContext.IsActive) + { + // Only create margin when in the context of a Git repository + return null; + } + + // Comments in the editor feature flag + if (!packageSettings.Value.EditorComments) + { + return null; + } + + // Never show on diff views + if (IsDiffView(wpfTextViewHost.TextView)) + { + return null; + } + + return new PullRequestFileMargin( + wpfTextViewHost.TextView, + enableInlineCommentsCommand.Value, + goToSolutionOrPullRequestFileCommand.Value, + sessionManager.Value, + usageTracker); + } + + bool IsDiffView(ITextView textView) => textView.Roles.Contains("DIFF"); + } +} diff --git a/src/GitHub.InlineReviews/Models/InlineCommentThreadModel.cs b/src/GitHub.InlineReviews/Models/InlineCommentThreadModel.cs new file mode 100644 index 0000000000..18fdf202b1 --- /dev/null +++ b/src/GitHub.InlineReviews/Models/InlineCommentThreadModel.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using GitHub.Extensions; +using GitHub.Models; +using ReactiveUI; + +namespace GitHub.InlineReviews.Models +{ + /// + /// Represents a thread of inline comments on an . + /// + class InlineCommentThreadModel : ReactiveObject, IInlineCommentThreadModel + { + bool isStale; + int lineNumber = -1; + + /// + /// Initializes a new instance of the class. + /// + /// The relative path to the file that the thread is on. + /// The SHA of the commit that the thread appears on. + /// + /// The last five lines of the thread's diff hunk, in reverse order. + /// + /// The comments in the thread + public InlineCommentThreadModel( + string relativePath, + string commitSha, + IList diffMatch, + IEnumerable comments) + { + Guard.ArgumentNotNull(relativePath, nameof(relativePath)); + Guard.ArgumentNotNull(commitSha, nameof(commitSha)); + Guard.ArgumentNotNull(diffMatch, nameof(diffMatch)); + + Comments = comments.ToList(); + DiffMatch = diffMatch; + DiffLineType = diffMatch[0].Type; + CommitSha = commitSha; + RelativePath = relativePath; + + foreach (var comment in comments) + { + comment.Thread = this; + } + } + + /// + public IReadOnlyList Comments { get; } + + /// + public IList DiffMatch { get; } + + /// + public DiffChangeType DiffLineType { get; } + + /// + public bool IsStale + { + get { return isStale; } + set { this.RaiseAndSetIfChanged(ref isStale, value); } + } + + /// + public int LineNumber + { + get { return lineNumber; } + set { this.RaiseAndSetIfChanged(ref lineNumber, value); } + } + + /// + public string CommitSha { get; } + + /// + public string RelativePath { get; } + } +} diff --git a/src/GitHub.InlineReviews/Models/PullRequestSessionFile.cs b/src/GitHub.InlineReviews/Models/PullRequestSessionFile.cs new file mode 100644 index 0000000000..a38b0d22c7 --- /dev/null +++ b/src/GitHub.InlineReviews/Models/PullRequestSessionFile.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Subjects; +using GitHub.Models; +using ReactiveUI; + +namespace GitHub.InlineReviews.Models +{ + /// + /// A file in a pull request session. + /// + /// + /// A holds the review comments for a file in a pull + /// request together with associated information such as the commit SHA of the file and the + /// diff with the file's merge base. + /// + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", + Justification = "linesChanged is shared and shouldn't be disposed")] + public class PullRequestSessionFile : ReactiveObject, IPullRequestSessionFile + { + readonly Subject>> linesChanged = new Subject>>(); + IReadOnlyList diff; + string commitSha; + IReadOnlyList inlineCommentThreads; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The relative path to the file in the repository. + /// + /// + /// The commit to pin the file to, or "HEAD" to follow the pull request head. + /// + public PullRequestSessionFile(string relativePath, string commitSha = "HEAD") + { + RelativePath = relativePath; + this.commitSha = commitSha; + IsTrackingHead = commitSha == "HEAD"; + } + + /// + public string RelativePath { get; } + + /// + public IReadOnlyList Diff + { + get { return diff; } + internal set { this.RaiseAndSetIfChanged(ref diff, value); } + } + + /// + public string BaseSha { get; internal set; } + + /// + public string CommitSha + { + get { return commitSha; } + internal set + { + if (value != commitSha) + { + if (!IsTrackingHead) + { + throw new GitHubLogicException( + "Cannot change the CommitSha of a PullRequestSessionFile that is not tracking HEAD."); + } + + this.RaiseAndSetIfChanged(ref commitSha, value); + } + } + } + + /// + public bool IsTrackingHead { get; } + + /// + public IReadOnlyList InlineCommentThreads + { + get { return inlineCommentThreads; } + set + { + var lines = (inlineCommentThreads ?? Enumerable.Empty())? + .Concat(value ?? Enumerable.Empty()) + .Select(x => Tuple.Create(x.LineNumber, x.DiffLineType == DiffChangeType.Delete ? DiffSide.Left : DiffSide.Right)) + .Where(x => x.Item1 >= 0) + .Distinct() + .ToList(); + this.RaisePropertyChanging(); + inlineCommentThreads = value; + this.RaisePropertyChanged(); + NotifyLinesChanged(lines); + } + } + + /// + public IObservable>> LinesChanged => linesChanged; + + /// + /// Raises the signal. + /// + /// The lines that have changed. + public void NotifyLinesChanged(IReadOnlyList> lines) => linesChanged.OnNext(lines); + } +} diff --git a/src/GitHub.InlineReviews/Models/PullRequestSessionLiveFile.cs b/src/GitHub.InlineReviews/Models/PullRequestSessionLiveFile.cs new file mode 100644 index 0000000000..f9df506ae7 --- /dev/null +++ b/src/GitHub.InlineReviews/Models/PullRequestSessionLiveFile.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Subjects; +using GitHub.Models; +using Microsoft.VisualStudio.Text; + +namespace GitHub.InlineReviews.Models +{ + /// + /// A file in a pull request session that tracks editor content. + /// + /// + /// A live session file extends to update the file's + /// review comments in real time, based on the contents of an editor and + /// . + /// + public sealed class PullRequestSessionLiveFile : PullRequestSessionFile, IDisposable + { + bool disposed = false; + + public PullRequestSessionLiveFile( + string relativePath, + ITextBuffer textBuffer, + ISubject rebuild) + : base(relativePath) + { + TextBuffer = textBuffer; + Rebuild = rebuild; + } + + /// + /// Gets the VS text buffer that the file is associated with. + /// + public ITextBuffer TextBuffer { get; } + + /// + /// Gets a resource to dispose. + /// + public IDisposable ToDispose { get; internal set; } + + /// + /// Gets a dictionary mapping review comments to tracking points in the . + /// + public IDictionary TrackingPoints { get; internal set; } + + /// + /// Gets an observable raised when the review comments for the file should be rebuilt. + /// + public ISubject Rebuild { get; } + + public void Dispose() + { + Dispose(true); + } + + /// + /// Disposes of the resources in . + /// + void Dispose(bool disposing) + { + if (!disposed) + { + disposed = true; + + if (disposing) + { + ToDispose?.Dispose(); + } + } + } + } +} diff --git a/src/GitHub.InlineReviews/Peek/InlineCommentPeekRelationship.cs b/src/GitHub.InlineReviews/Peek/InlineCommentPeekRelationship.cs new file mode 100644 index 0000000000..826b0002aa --- /dev/null +++ b/src/GitHub.InlineReviews/Peek/InlineCommentPeekRelationship.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.VisualStudio.Language.Intellisense; + +namespace GitHub.InlineReviews.Peek +{ + class InlineCommentPeekRelationship : IPeekRelationship + { + static InlineCommentPeekRelationship instance; + + private InlineCommentPeekRelationship() + { + } + + public static InlineCommentPeekRelationship Instance + { + get + { + if (instance == null) + { + instance = new InlineCommentPeekRelationship(); + } + + return instance; + } + } + + public string DisplayName => "GitHub Code Review"; + public string Name => "GitHubCodeReview"; + } +} diff --git a/src/GitHub.InlineReviews/Peek/InlineCommentPeekResult.cs b/src/GitHub.InlineReviews/Peek/InlineCommentPeekResult.cs new file mode 100644 index 0000000000..ac9394b78b --- /dev/null +++ b/src/GitHub.InlineReviews/Peek/InlineCommentPeekResult.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.VisualStudio.Language.Intellisense; +using GitHub.Extensions; +using GitHub.InlineReviews.ViewModels; + +namespace GitHub.InlineReviews.Peek +{ + sealed class InlineCommentPeekResult : IPeekResult + { + public InlineCommentPeekResult(InlineCommentPeekViewModel viewModel) + { + Guard.ArgumentNotNull(viewModel, nameof(viewModel)); + + this.ViewModel = viewModel; + } + + public bool CanNavigateTo => true; + public InlineCommentPeekViewModel ViewModel { get; } + + public IPeekResultDisplayInfo DisplayInfo { get; } + = new PeekResultDisplayInfo("Review", null, "GitHub Review", "GitHub Review"); + + public Action PostNavigationCallback => null; + + public event EventHandler Disposed; + + public void Dispose() + { + Disposed?.Invoke(this, EventArgs.Empty); + } + + public void NavigateTo(object data) + { + } + } +} diff --git a/src/GitHub.InlineReviews/Peek/InlineCommentPeekResultPresentation.cs b/src/GitHub.InlineReviews/Peek/InlineCommentPeekResultPresentation.cs new file mode 100644 index 0000000000..3aefe50256 --- /dev/null +++ b/src/GitHub.InlineReviews/Peek/InlineCommentPeekResultPresentation.cs @@ -0,0 +1,105 @@ +using System; +using System.Windows; +using Microsoft.VisualStudio.Language.Intellisense; +using GitHub.InlineReviews.ViewModels; +using GitHub.InlineReviews.Views; + +namespace GitHub.InlineReviews.Peek +{ + class InlineCommentPeekResultPresentation : IPeekResultPresentation, IDesiredHeightProvider + { + const double PeekBorders = 28.0; + readonly InlineCommentPeekViewModel viewModel; + InlineCommentPeekView view; + double desiredHeight; + + public bool IsDirty => false; + public bool IsReadOnly => true; + + public InlineCommentPeekResultPresentation(InlineCommentPeekViewModel viewModel) + { + this.viewModel = viewModel; + } + + public double ZoomLevel + { + get { return 1.0; } + set { } + } + + public double DesiredHeight + { + get { return desiredHeight; } + private set + { + if (desiredHeight != value && DesiredHeightChanged != null) + { + desiredHeight = value; + DesiredHeightChanged(this, EventArgs.Empty); + } + } + } + + public event EventHandler IsDirtyChanged + { + add { } + remove { } + } + + public event EventHandler IsReadOnlyChanged + { + add { } + remove { } + } + + public event EventHandler RecreateContent = delegate { }; + public event EventHandler DesiredHeightChanged; + + public bool CanSave(out string defaultPath) + { + defaultPath = null; + return false; + } + + public IPeekResultScrollState CaptureScrollState() + { + return null; + } + + public void Close() + { + } + + public UIElement Create(IPeekSession session, IPeekResultScrollState scrollState) + { + view = new InlineCommentPeekView(); + view.DataContext = viewModel; + + // Report the desired size back to the peek view. Unfortunately the peek view + // helpfully assigns this desired size to the control that also contains the tab at + // the top of the peek view, so we need to put in a fudge factor. Using a const + // value for the moment, as there's no easy way to get the size of the control. + view.DesiredHeight.Subscribe(x => DesiredHeight = x + PeekBorders); + + return view; + } + + public void Dispose() + { + } + + public void ScrollIntoView(IPeekResultScrollState scrollState) + { + } + + public void SetKeyboardFocus() + { + } + + public bool TryOpen(IPeekResult otherResult) => false; + + public bool TryPrepareToClose() => true; + + public bool TrySave(bool saveAs) => true; + } +} diff --git a/src/GitHub.InlineReviews/Peek/InlineCommentPeekResultPresenter.cs b/src/GitHub.InlineReviews/Peek/InlineCommentPeekResultPresenter.cs new file mode 100644 index 0000000000..2c0ee81087 --- /dev/null +++ b/src/GitHub.InlineReviews/Peek/InlineCommentPeekResultPresenter.cs @@ -0,0 +1,20 @@ +using System; +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Language.Intellisense; +using Microsoft.VisualStudio.Utilities; + +namespace GitHub.InlineReviews.Peek +{ + [Export(typeof(IPeekResultPresenter))] + [Name("GitHub Inline Comments Peek Presenter")] + class InlineCommentPeekResultPresenter : IPeekResultPresenter + { + public IPeekResultPresentation TryCreatePeekResultPresentation(IPeekResult result) + { + var review = result as InlineCommentPeekResult; + return review != null ? + new InlineCommentPeekResultPresentation(review.ViewModel) : + null; + } + } +} diff --git a/src/GitHub.InlineReviews/Peek/InlineCommentPeekableItem.cs b/src/GitHub.InlineReviews/Peek/InlineCommentPeekableItem.cs new file mode 100644 index 0000000000..2f07f939f2 --- /dev/null +++ b/src/GitHub.InlineReviews/Peek/InlineCommentPeekableItem.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using Microsoft.VisualStudio.Language.Intellisense; +using GitHub.InlineReviews.ViewModels; + +namespace GitHub.InlineReviews.Peek +{ + class InlineCommentPeekableItem : IPeekableItem + { + public InlineCommentPeekableItem(InlineCommentPeekViewModel viewModel) + { + ViewModel = viewModel; + } + + public string DisplayName => "GitHub Code Review"; + public InlineCommentPeekViewModel ViewModel { get; } + + public IEnumerable Relationships => new[] { InlineCommentPeekRelationship.Instance }; + + public IPeekResultSource GetOrCreateResultSource(string relationshipName) + { + return new InlineCommentPeekableResultSource(ViewModel); + } + } +} diff --git a/src/GitHub.InlineReviews/Peek/InlineCommentPeekableItemSource.cs b/src/GitHub.InlineReviews/Peek/InlineCommentPeekableItemSource.cs new file mode 100644 index 0000000000..21e63fbddd --- /dev/null +++ b/src/GitHub.InlineReviews/Peek/InlineCommentPeekableItemSource.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using GitHub.Commands; +using GitHub.Extensions; +using GitHub.Factories; +using GitHub.InlineReviews.Commands; +using GitHub.InlineReviews.Services; +using GitHub.InlineReviews.ViewModels; +using GitHub.Services; +using Microsoft.VisualStudio.Language.Intellisense; + +namespace GitHub.InlineReviews.Peek +{ + class InlineCommentPeekableItemSource : IPeekableItemSource + { + readonly IInlineCommentPeekService peekService; + readonly IPullRequestSessionManager sessionManager; + readonly INextInlineCommentCommand nextCommentCommand; + readonly IPreviousInlineCommentCommand previousCommentCommand; + readonly ICommentService commentService; + + public InlineCommentPeekableItemSource(IInlineCommentPeekService peekService, + IPullRequestSessionManager sessionManager, + INextInlineCommentCommand nextCommentCommand, + IPreviousInlineCommentCommand previousCommentCommand, + ICommentService commentService) + { + this.peekService = peekService; + this.sessionManager = sessionManager; + this.nextCommentCommand = nextCommentCommand; + this.previousCommentCommand = previousCommentCommand; + this.commentService = commentService; + } + + public void AugmentPeekSession(IPeekSession session, IList peekableItems) + { + if (session.RelationshipName == InlineCommentPeekRelationship.Instance.Name) + { + var viewModel = new InlineCommentPeekViewModel( + peekService, + session, + sessionManager, + nextCommentCommand, + previousCommentCommand, + commentService); + viewModel.Initialize().Forget(); + peekableItems.Add(new InlineCommentPeekableItem(viewModel)); + } + } + + public void Dispose() + { + } + } +} diff --git a/src/GitHub.InlineReviews/Peek/InlineCommentPeekableItemSourceProvider.cs b/src/GitHub.InlineReviews/Peek/InlineCommentPeekableItemSourceProvider.cs new file mode 100644 index 0000000000..65558b3d83 --- /dev/null +++ b/src/GitHub.InlineReviews/Peek/InlineCommentPeekableItemSourceProvider.cs @@ -0,0 +1,50 @@ +using System; +using System.ComponentModel.Composition; +using GitHub.Commands; +using GitHub.Factories; +using GitHub.InlineReviews.Commands; +using GitHub.InlineReviews.Services; +using GitHub.Services; +using Microsoft.VisualStudio.Language.Intellisense; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Utilities; + +namespace GitHub.InlineReviews.Peek +{ + [Export(typeof(IPeekableItemSourceProvider))] + [ContentType("text")] + [Name("GitHub Inline Comments Peekable Item Source")] + class InlineCommentPeekableItemSourceProvider : IPeekableItemSourceProvider + { + readonly IInlineCommentPeekService peekService; + readonly IPullRequestSessionManager sessionManager; + readonly INextInlineCommentCommand nextCommentCommand; + readonly IPreviousInlineCommentCommand previousCommentCommand; + readonly ICommentService commentService; + + [ImportingConstructor] + public InlineCommentPeekableItemSourceProvider( + IInlineCommentPeekService peekService, + IPullRequestSessionManager sessionManager, + INextInlineCommentCommand nextCommentCommand, + IPreviousInlineCommentCommand previousCommentCommand, + ICommentService commentService) + { + this.peekService = peekService; + this.sessionManager = sessionManager; + this.nextCommentCommand = nextCommentCommand; + this.previousCommentCommand = previousCommentCommand; + this.commentService = commentService; + } + + public IPeekableItemSource TryCreatePeekableItemSource(ITextBuffer textBuffer) + { + return new InlineCommentPeekableItemSource( + peekService, + sessionManager, + nextCommentCommand, + previousCommentCommand, + commentService); + } + } +} diff --git a/src/GitHub.InlineReviews/Peek/InlineCommentPeekableResultSource.cs b/src/GitHub.InlineReviews/Peek/InlineCommentPeekableResultSource.cs new file mode 100644 index 0000000000..b15dddbb60 --- /dev/null +++ b/src/GitHub.InlineReviews/Peek/InlineCommentPeekableResultSource.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading; +using Microsoft.VisualStudio.Language.Intellisense; +using GitHub.InlineReviews.ViewModels; + +namespace GitHub.InlineReviews.Peek +{ + class InlineCommentPeekableResultSource : IPeekResultSource + { + readonly InlineCommentPeekViewModel viewModel; + + public InlineCommentPeekableResultSource(InlineCommentPeekViewModel viewModel) + { + this.viewModel = viewModel; + } + + public void FindResults(string relationshipName, IPeekResultCollection resultCollection, CancellationToken cancellationToken, IFindPeekResultsCallback callback) + { + resultCollection.Add(new InlineCommentPeekResult(viewModel)); + } + } +} diff --git a/src/GitHub.InlineReviews/Properties/AssemblyInfo.cs b/src/GitHub.InlineReviews/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..f4657e8520 --- /dev/null +++ b/src/GitHub.InlineReviews/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("GitHub.InlineReviews")] +[assembly: AssemblyDescription("Provides inline viewing of PR review comments")] +[assembly: Guid("3bf91177-3d16-425d-9c62-50a86cf26298")] diff --git a/src/GitHub.InlineReviews/Properties/DesignTimeResources.xaml b/src/GitHub.InlineReviews/Properties/DesignTimeResources.xaml new file mode 100644 index 0000000000..389ecf3081 --- /dev/null +++ b/src/GitHub.InlineReviews/Properties/DesignTimeResources.xaml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/GitHub.InlineReviews/PullRequestStatusBarPackage.cs b/src/GitHub.InlineReviews/PullRequestStatusBarPackage.cs new file mode 100644 index 0000000000..55eeb756e6 --- /dev/null +++ b/src/GitHub.InlineReviews/PullRequestStatusBarPackage.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading; +using System.Runtime.InteropServices; +using GitHub.VisualStudio; +using GitHub.InlineReviews.Services; +using Microsoft.VisualStudio.Shell; +using Task = System.Threading.Tasks.Task; +using Microsoft.VisualStudio.Threading; +using Microsoft.VisualStudio.ComponentModelHost; + +namespace GitHub.InlineReviews +{ + [Guid(Guids.PullRequestStatusPackageId)] + [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] + [ProvideAutoLoad(Guids.UIContext_Git, 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) + { + // Avoid delays when there is ongoing UI activity. + // See: https://github.com/github/VisualStudio/issues/1537 + await JoinableTaskFactory.RunAsync(VsTaskRunContext.UIThreadNormalPriority, InitializeStatusBar); + } + + async Task InitializeStatusBar() + { + var componentModel = (IComponentModel)(await GetServiceAsync(typeof(SComponentModel))); + var exports = componentModel.DefaultExportProvider; + var barManager = exports.GetExportedValue(); + + await JoinableTaskFactory.SwitchToMainThreadAsync(); + barManager.StartShowingStatus(); + } + } +} diff --git a/src/GitHub.InlineReviews/Resources/logo_32x32@2x.png b/src/GitHub.InlineReviews/Resources/logo_32x32@2x.png new file mode 100644 index 0000000000..1fd18c1c7a Binary files /dev/null and b/src/GitHub.InlineReviews/Resources/logo_32x32@2x.png differ diff --git a/src/GitHub.InlineReviews/SampleData/CommentThreadViewModelDesigner.cs b/src/GitHub.InlineReviews/SampleData/CommentThreadViewModelDesigner.cs new file mode 100644 index 0000000000..eac1dca542 --- /dev/null +++ b/src/GitHub.InlineReviews/SampleData/CommentThreadViewModelDesigner.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Reactive; +using GitHub.InlineReviews.ViewModels; +using GitHub.Models; +using GitHub.ViewModels; +using ReactiveUI; + +namespace GitHub.InlineReviews.SampleData +{ + [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses")] + class CommentThreadViewModelDesigner : ICommentThreadViewModel + { + public ObservableCollection Comments { get; } + = new ObservableCollection(); + + public IActorViewModel CurrentUser { get; set; } + = new ActorViewModel { Login = "shana" }; + + public ReactiveCommand PostComment { get; } + public ReactiveCommand EditComment { get; } + public ReactiveCommand DeleteComment { get; } + } +} diff --git a/src/GitHub.InlineReviews/SampleData/CommentViewModelDesigner.cs b/src/GitHub.InlineReviews/SampleData/CommentViewModelDesigner.cs new file mode 100644 index 0000000000..ddd907015c --- /dev/null +++ b/src/GitHub.InlineReviews/SampleData/CommentViewModelDesigner.cs @@ -0,0 +1,38 @@ +using System; +using System.Reactive; +using System.Diagnostics.CodeAnalysis; +using GitHub.InlineReviews.ViewModels; +using ReactiveUI; +using GitHub.ViewModels; + +namespace GitHub.InlineReviews.SampleData +{ + [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses")] + class CommentViewModelDesigner : ReactiveObject, ICommentViewModel + { + public CommentViewModelDesigner() + { + Author = new ActorViewModel { Login = "shana" }; + } + + public string Id { get; set; } + public int PullRequestId { get; set; } + public int DatabaseId { get; set; } + public string Body { get; set; } + public string ErrorMessage { get; set; } + public CommentEditState EditState { get; set; } + public bool IsReadOnly { get; set; } + public bool IsSubmitting { get; set; } + public bool CanDelete { get; } = true; + public ICommentThreadViewModel Thread { get; } + public DateTimeOffset UpdatedAt => DateTime.Now.Subtract(TimeSpan.FromDays(3)); + public IActorViewModel Author { get; set; } + public Uri WebUrl { get; } + + public ReactiveCommand BeginEdit { get; } + public ReactiveCommand CancelEdit { get; } + public ReactiveCommand CommitEdit { get; } + public ReactiveCommand OpenOnGitHub { get; } + public ReactiveCommand Delete { get; } + } +} diff --git a/src/GitHub.InlineReviews/Services/CommentService.cs b/src/GitHub.InlineReviews/Services/CommentService.cs new file mode 100644 index 0000000000..566c4b764e --- /dev/null +++ b/src/GitHub.InlineReviews/Services/CommentService.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.Composition; +using System.Windows.Forms; + +namespace GitHub.InlineReviews.Services +{ + [Export(typeof(ICommentService))] + [PartCreationPolicy(CreationPolicy.NonShared)] + public class CommentService:ICommentService + { + public bool ConfirmCommentDelete() + { + return MessageBox.Show( + VisualStudio.UI.Resources.DeleteCommentConfirmation, + VisualStudio.UI.Resources.DeleteCommentConfirmationCaption, + MessageBoxButtons.YesNo, + MessageBoxIcon.Question) == DialogResult.Yes; + } + } +} \ No newline at end of file diff --git a/src/GitHub.InlineReviews/Services/DiffService.cs b/src/GitHub.InlineReviews/Services/DiffService.cs new file mode 100644 index 0000000000..625bf7492e --- /dev/null +++ b/src/GitHub.InlineReviews/Services/DiffService.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Threading.Tasks; +using GitHub.Models; +using GitHub.Services; +using LibGit2Sharp; + +namespace GitHub.InlineReviews.Services +{ + /// + /// Service for generating parsed diffs. + /// + [Export(typeof(IDiffService))] + [PartCreationPolicy(CreationPolicy.NonShared)] + public class DiffService : IDiffService + { + readonly IGitClient gitClient; + + [ImportingConstructor] + public DiffService(IGitClient gitClient) + { + this.gitClient = gitClient; + } + + /// + public async Task> Diff( + IRepository repo, + string baseSha, + string headSha, + string path) + { + var patch = await gitClient.Compare(repo, baseSha, headSha, path); + + if (patch != null) + { + return DiffUtilities.ParseFragment(patch).ToList(); + } + else + { + return new DiffChunk[0]; + } + } + + /// + public async Task> Diff( + IRepository repo, + string baseSha, + string headSha, + string path, + byte[] contents) + { + var changes = await gitClient.CompareWith(repo, baseSha, headSha, path, contents); + + if (changes?.Patch != null) + { + return DiffUtilities.ParseFragment(changes.Patch).ToList(); + } + else + { + return new DiffChunk[0]; + } + } + } +} diff --git a/src/GitHub.InlineReviews/Services/ICommentService.cs b/src/GitHub.InlineReviews/Services/ICommentService.cs new file mode 100644 index 0000000000..a206eb2ac4 --- /dev/null +++ b/src/GitHub.InlineReviews/Services/ICommentService.cs @@ -0,0 +1,14 @@ +namespace GitHub.InlineReviews.Services +{ + /// + /// This service allows for functionality to be injected into the chain of different peek Comment ViewModel types. + /// + public interface ICommentService + { + /// + /// This function uses MessageBox.Show to display a confirmation if a comment should be deleted. + /// + /// + bool ConfirmCommentDelete(); + } +} \ No newline at end of file diff --git a/src/GitHub.InlineReviews/Services/IDiffService.cs b/src/GitHub.InlineReviews/Services/IDiffService.cs new file mode 100644 index 0000000000..c8c76aeeed --- /dev/null +++ b/src/GitHub.InlineReviews/Services/IDiffService.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using GitHub.Models; +using LibGit2Sharp; + +namespace GitHub.InlineReviews.Services +{ + /// + /// Service for generating parsed diffs. + /// + public interface IDiffService + { + /// + /// Calculates the diff of a file in a repository between two commits. + /// + /// The repository + /// The base commit SHA. + /// The head commit SHA. + /// The path to the file in the repository. + /// + /// A collection of s containing the parsed diff. + /// + Task> Diff(IRepository repo, string baseSha, string headSha, string relativePath); + + /// + /// Calculates the diff of a file in a repository between a base commit and a byte arrat. + /// + /// The repository + /// The base commit SHA. + /// The head commit SHA. + /// The path to the file in the repository. + /// The byte array to compare with the base SHA. + /// + /// A collection of s containing the parsed diff. + /// + /// + /// Note that even though the comparison is done between and + /// , the still needs to be provided in order + /// to track renames. + /// + Task> Diff(IRepository repo, string baseSha, string headSha, string relativePath, byte[] contents); + } +} \ No newline at end of file diff --git a/src/GitHub.InlineReviews/Services/IInlineCommentPeekService.cs b/src/GitHub.InlineReviews/Services/IInlineCommentPeekService.cs new file mode 100644 index 0000000000..5351d64a2d --- /dev/null +++ b/src/GitHub.InlineReviews/Services/IInlineCommentPeekService.cs @@ -0,0 +1,45 @@ +using System; +using GitHub.InlineReviews.Tags; +using Microsoft.VisualStudio.Language.Intellisense; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; + +namespace GitHub.InlineReviews.Services +{ + /// + /// Shows inline comments in a peek view. + /// + public interface IInlineCommentPeekService + { + /// + /// Gets the line number for a peek session tracking point. + /// + /// The peek session. + /// The peek session tracking point + /// + /// A tuple containing the line number and whether the line number represents a line in the + /// left hand side of a diff view. + /// + Tuple GetLineNumber(IPeekSession session, ITrackingPoint point); + + /// + /// Hides the inline comment peek view for a text view. + /// + /// The text view. + void Hide(ITextView textView); + + /// + /// Shows the peek view for a . + /// + /// The text view. + /// The tag. + ITrackingPoint Show(ITextView textView, ShowInlineCommentTag tag); + + /// + /// Shows the peek view for an . + /// + /// The text view. + /// The tag. + ITrackingPoint Show(ITextView textView, AddInlineCommentTag tag); + } +} \ No newline at end of file diff --git a/src/GitHub.InlineReviews/Services/IPullRequestSessionService.cs b/src/GitHub.InlineReviews/Services/IPullRequestSessionService.cs new file mode 100644 index 0000000000..a867a4393a --- /dev/null +++ b/src/GitHub.InlineReviews/Services/IPullRequestSessionService.cs @@ -0,0 +1,347 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Subjects; +using System.Threading.Tasks; +using GitHub.Models; +using GitHub.Primitives; +using Microsoft.VisualStudio.Text; +using Octokit; + +namespace GitHub.InlineReviews.Services +{ + /// + /// Provides a common interface for services required by . + /// + public interface IPullRequestSessionService + { + /// + /// Carries out a diff of a file between two commits. + /// + /// The repository. + /// The commit to use as the base. + /// The commit to use as the head. + /// The relative path to the file. + /// + Task> Diff( + ILocalRepositoryModel repository, + string baseSha, + string headSha, + string relativePath); + + /// + /// Carries out a diff between a file at a commit and the current file contents. + /// + /// The repository. + /// The commit to use as the base. + /// The commit to use as the head. + /// The relative path to the file. + /// The contents of the file. + /// + Task> Diff( + ILocalRepositoryModel repository, + string baseSha, + string headSha, + string relativePath, + byte[] contents); + + /// + /// Builds a set of comment thread models for a file based on a pull request model and a diff. + /// + /// The pull request session. + /// The relative path to the file. + /// The diff. + /// The SHA of the HEAD. + /// + /// A collection of objects with updated line numbers. + /// + IReadOnlyList BuildCommentThreads( + PullRequestDetailModel pullRequest, + string relativePath, + IReadOnlyList diff, + string headSha); + + /// + /// Updates a set of comment thread models for a file based on a new diff. + /// + /// The theads to update. + /// The diff. + /// + /// A collection of updated line numbers. + /// + IReadOnlyList> UpdateCommentThreads( + IReadOnlyList threads, + IReadOnlyList diff); + + /// + /// Tests whether the contents of a file represent a commit that is pushed to origin. + /// + /// The repository. + /// The relative path to the file. + /// The contents of the file. + /// + /// A task returning true if the file is unmodified with respect to the latest commit + /// pushed to origin; otherwise false. + /// + Task IsUnmodifiedAndPushed( + ILocalRepositoryModel repository, + string relativePath, + byte[] contents); + + /// + /// Extracts a file at a specified commit from the repository. + /// + /// The repository. + /// The pull request number + /// The SHA of the commit. + /// The path to the file, relative to the repository. + /// + /// The contents of the file, or null if the file was not found at the specified commit. + /// + Task ExtractFileFromGit( + ILocalRepositoryModel repository, + int pullRequestNumber, + string sha, + string relativePath); + + /// + /// Gets the associated for an . + /// + /// The buffer. + /// + /// The associated document, or null if not found. + /// + ITextDocument GetDocument(ITextBuffer buffer); + + /// + /// Gets the contents of an using the buffer's current encoding. + /// + /// The buffer. + /// The contents of the buffer. + byte[] GetContents(ITextBuffer buffer); + + /// + /// Gets the SHA of the tip of the current branch. + /// + /// The repository. + /// The tip SHA. + Task GetTipSha(ILocalRepositoryModel repository); + + /// + /// Asynchronously reads the contents of a file. + /// + /// The full path to the file. + /// + /// A task returning the contents of the file, or null if the file was not found. + /// + Task ReadFileAsync(string path); + + /// + /// Reads a for a specified pull request. + /// + /// The host address. + /// The repository owner. + /// The repository name. + /// The pull request number. + /// A task returning the pull request model. + Task ReadPullRequestDetail(HostAddress address, string owner, string name, int number); + + /// + /// Reads the current viewer for the specified address.. + /// + /// The host address. + /// A task returning the viewer. + /// + /// A "Viewer" is the GraphQL term for the currently authenticated user. + /// + Task ReadViewer(HostAddress address); + + /// + /// Find the merge base for a pull request. + /// + /// The repository. + /// The pull request. + /// + /// The merge base SHA for the PR. + /// + Task GetPullRequestMergeBase(ILocalRepositoryModel repository, PullRequestDetailModel pullRequest); + + /// + /// Gets the GraphQL ID for a pull request. + /// + /// The local repository. + /// The owner of the remote fork. + /// The pull request number. + /// + Task GetGraphQLPullRequestId( + ILocalRepositoryModel localRepository, + string repositoryOwner, + int number); + + /// + /// Creates a rebuild signal subject for a . + /// + /// + /// A subject which is used to signal a rebuild of a live file. + /// + /// + /// The creation of the rebuild signal for a is + /// abstracted out into this service for unit testing. The default behavior of this subject + /// is to throttle the signal for 500ms so that the live file is not updated continuously + /// while the user is typing. + /// + ISubject CreateRebuildSignal(); + + /// + /// Creates a new pending review on the server. + /// + /// The local repository. + /// The user posting the review. + /// The GraphQL ID of the pull request. + /// The updated state of the pull request. + Task CreatePendingReview( + ILocalRepositoryModel localRepository, + string pullRequestId); + + /// + /// Cancels a pending review on the server. + /// + /// The local repository. + /// The GraphQL ID of the review. + /// The updated state of the pull request. + Task CancelPendingReview( + ILocalRepositoryModel localRepository, + string reviewId); + + /// + /// Posts PR review with no comments. + /// + /// The local repository. + /// The GraphQL ID of the pull request. + /// The SHA of the commit being reviewed. + /// The review body. + /// The review event. + /// The updated state of the pull request. + Task PostReview( + ILocalRepositoryModel localRepository, + string pullRequestId, + string commitId, + string body, + PullRequestReviewEvent e); + + /// + /// Submits a pending PR review. + /// + /// The local repository. + /// The GraphQL ID of the pending review. + /// The review body. + /// The review event. + /// The updated state of the pull request. + Task SubmitPendingReview( + ILocalRepositoryModel localRepository, + string pendingReviewId, + string body, + PullRequestReviewEvent e); + + /// + /// Posts a new pending PR review comment. + /// + /// The local repository. + /// The GraphQL ID of the pending review. + /// The comment body. + /// THe SHA of the commit to comment on. + /// The relative path of the file to comment on. + /// The line index in the diff to comment on. + /// The updated state of the pull request. + /// + /// This method posts a new pull request comment to a pending review started by + /// . + /// + Task PostPendingReviewComment( + ILocalRepositoryModel localRepository, + string pendingReviewId, + string body, + string commitId, + string path, + int position); + + /// + /// Posts a new pending PR review comment reply. + /// + /// The local repository. + /// The GraphQL ID of the pending review. + /// The comment body. + /// The GraphQL ID of the comment to reply to. + /// The updated state of the pull request. + /// + /// The method posts a new pull request comment to a pending review started by + /// . + /// + Task PostPendingReviewCommentReply( + ILocalRepositoryModel localRepository, + string pendingReviewId, + string body, + string inReplyTo); + + /// + /// Posts a new standalone PR review comment. + /// + /// The local repository. + /// The GraphQL ID of the pull request. + /// The comment body. + /// THe SHA of the commit to comment on. + /// The relative path of the file to comment on. + /// The line index in the diff to comment on. + /// The updated state of the pull request. + /// + /// The method posts a new standalone pull request comment that is not attached to a pending + /// pull request review. + /// + Task PostStandaloneReviewComment( + ILocalRepositoryModel localRepository, + string pullRequestId, + string body, + string commitId, + string path, + int position); + + /// + /// Posts a PR review comment reply. + /// + /// The local repository. + /// The GraphQL ID of the pull request. + /// The comment body. + /// The GraphQL ID of the comment to reply to. + /// The updated state of the pull request. + Task PostStandaloneReviewCommentReply( + ILocalRepositoryModel localRepository, + string pullRequestId, + string body, + string inReplyTo); + + /// + /// Delete a PR review comment. + /// + /// The local repository. + /// The owner of the repository. + /// The pull request id of the comment + /// The pull request comment number. + /// The updated state of the pull request. + Task DeleteComment(ILocalRepositoryModel localRepository, + string remoteRepositoryOwner, + int pullRequestId, + int commentDatabaseId); + + /// + /// Edit a PR review comment. + /// + /// The local repository. + /// The owner of the repository. + /// The pull request comment node id. + /// The replacement comment body. + /// The updated state of the pull request. + Task EditComment(ILocalRepositoryModel localRepository, + string remoteRepositoryOwner, + string commentNodeId, + string body); + } +} diff --git a/src/GitHub.InlineReviews/Services/InlineCommentPeekService.cs b/src/GitHub.InlineReviews/Services/InlineCommentPeekService.cs new file mode 100644 index 0000000000..892a7c47b1 --- /dev/null +++ b/src/GitHub.InlineReviews/Services/InlineCommentPeekService.cs @@ -0,0 +1,175 @@ +using System; +using System.ComponentModel.Composition; +using System.Linq; +using System.Reactive.Linq; +using System.Threading.Tasks; +using GitHub.Api; +using GitHub.Extensions; +using GitHub.Factories; +using GitHub.InlineReviews.Peek; +using GitHub.InlineReviews.Tags; +using GitHub.Models; +using GitHub.Primitives; +using GitHub.Services; +using Microsoft.VisualStudio.Language.Intellisense; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Differencing; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Outlining; +using Microsoft.VisualStudio.Text.Projection; + +namespace GitHub.InlineReviews.Services +{ + /// + /// Shows inline comments in a peek view. + /// + [Export(typeof(IInlineCommentPeekService))] + class InlineCommentPeekService : IInlineCommentPeekService + { + readonly IOutliningManagerService outliningService; + readonly IPeekBroker peekBroker; + readonly IUsageTracker usageTracker; + + [ImportingConstructor] + public InlineCommentPeekService( + IOutliningManagerService outliningManager, + IPeekBroker peekBroker, + IUsageTracker usageTracker) + { + this.outliningService = outliningManager; + this.peekBroker = peekBroker; + this.usageTracker = usageTracker; + } + + /// + public Tuple GetLineNumber(IPeekSession session, ITrackingPoint point) + { + var diffModel = (session.TextView as IWpfTextView)?.TextViewModel as IDifferenceTextViewModel; + var leftBuffer = false; + ITextSnapshotLine line = null; + + if (diffModel != null) + { + if (diffModel.ViewType == DifferenceViewType.InlineView) + { + // If we're displaying a diff in inline mode, then we need to map the point down + // to the left or right buffer. + var snapshotPoint = point.GetPoint(point.TextBuffer.CurrentSnapshot); + var mappedPoint = session.TextView.BufferGraph.MapDownToFirstMatch( + snapshotPoint, + PointTrackingMode.Negative, + x => !(x is IProjectionSnapshot), + PositionAffinity.Successor); + + if (mappedPoint != null) + { + leftBuffer = mappedPoint.Value.Snapshot == diffModel.Viewer.DifferenceBuffer.LeftBuffer.CurrentSnapshot; + line = mappedPoint.Value.GetContainingLine(); + } + } + else + { + // If we're displaying a diff in any other mode than inline, then we're in the + // left buffer if the session's text view is the diff's left view. + leftBuffer = session.TextView == diffModel.Viewer.LeftView; + } + } + + if (line == null) + { + line = point.GetPoint(point.TextBuffer.CurrentSnapshot).GetContainingLine(); + } + + return Tuple.Create(line.LineNumber, leftBuffer); + } + + /// + public void Hide(ITextView textView) + { + peekBroker.DismissPeekSession(textView); + } + + /// + public ITrackingPoint Show(ITextView textView, AddInlineCommentTag tag) + { + Guard.ArgumentNotNull(tag, nameof(tag)); + + var lineAndtrackingPoint = GetLineAndTrackingPoint(textView, tag); + var line = lineAndtrackingPoint.Item1; + var trackingPoint = lineAndtrackingPoint.Item2; + var options = new PeekSessionCreationOptions( + textView, + InlineCommentPeekRelationship.Instance.Name, + trackingPoint, + defaultHeight: 0); + + ExpandCollapsedRegions(textView, line.Extent); + + var session = peekBroker.TriggerPeekSession(options); + var item = session.PeekableItems.OfType().FirstOrDefault(); + item?.ViewModel.Close.Take(1).Subscribe(_ => session.Dismiss()); + + return trackingPoint; + } + + /// + public ITrackingPoint Show(ITextView textView, ShowInlineCommentTag tag) + { + Guard.ArgumentNotNull(textView, nameof(textView)); + Guard.ArgumentNotNull(tag, nameof(tag)); + + var lineAndtrackingPoint = GetLineAndTrackingPoint(textView, tag); + var line = lineAndtrackingPoint.Item1; + var trackingPoint = lineAndtrackingPoint.Item2; + var options = new PeekSessionCreationOptions( + textView, + InlineCommentPeekRelationship.Instance.Name, + trackingPoint, + defaultHeight: 0); + + ExpandCollapsedRegions(textView, line.Extent); + + var session = peekBroker.TriggerPeekSession(options); + var item = session.PeekableItems.OfType().FirstOrDefault(); + item?.ViewModel.Close.Take(1).Subscribe(_ => session.Dismiss()); + + return trackingPoint; + } + + Tuple GetLineAndTrackingPoint(ITextView textView, InlineCommentTag tag) + { + var diffModel = (textView as IWpfTextView)?.TextViewModel as IDifferenceTextViewModel; + var snapshot = textView.TextSnapshot; + + if (diffModel?.ViewType == DifferenceViewType.InlineView) + { + snapshot = tag.DiffChangeType == DiffChangeType.Delete ? + diffModel.Viewer.DifferenceBuffer.LeftBuffer.CurrentSnapshot : + diffModel.Viewer.DifferenceBuffer.RightBuffer.CurrentSnapshot; + } + + var line = snapshot.GetLineFromLineNumber(tag.LineNumber); + var trackingPoint = snapshot.CreateTrackingPoint(line.Start.Position, PointTrackingMode.Positive); + + ExpandCollapsedRegions(textView, line.Extent); + peekBroker.TriggerPeekSession(textView, trackingPoint, InlineCommentPeekRelationship.Instance.Name); + + usageTracker.IncrementCounter(x => x.NumberOfPRReviewDiffViewInlineCommentOpen).Forget(); + + return Tuple.Create(line, trackingPoint); + } + + void ExpandCollapsedRegions(ITextView textView, SnapshotSpan span) + { + var outlining = outliningService.GetOutliningManager(textView); + + if (outlining != null) + { + foreach (var collapsed in outlining.GetCollapsedRegions(span)) + { + outlining.Expand(collapsed); + } + } + } + } +} diff --git a/src/GitHub.InlineReviews/Services/PullRequestSession.cs b/src/GitHub.InlineReviews/Services/PullRequestSession.cs new file mode 100644 index 0000000000..0759b996d2 --- /dev/null +++ b/src/GitHub.InlineReviews/Services/PullRequestSession.cs @@ -0,0 +1,425 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using GitHub.Extensions; +using GitHub.InlineReviews.Models; +using GitHub.Models; +using GitHub.Services; +using ReactiveUI; +using System.Threading; +using System.Reactive.Subjects; +using static System.FormattableString; +using GitHub.Primitives; + +namespace GitHub.InlineReviews.Services +{ + /// + /// A pull request session used to display inline reviews. + /// + /// + /// A pull request session represents the real-time state of a pull request in the IDE. + /// It takes the pull request model and updates according to the current state of the + /// repository on disk and in the editor. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", + Justification = "PullRequestSession is shared and shouldn't be disposed")] + public class PullRequestSession : ReactiveObject, IPullRequestSession + { + readonly IPullRequestSessionService service; + readonly Dictionary fileIndex = new Dictionary(); + readonly SemaphoreSlim getFilesLock = new SemaphoreSlim(1); + bool isCheckedOut; + string mergeBase; + IReadOnlyList files; + PullRequestDetailModel pullRequest; + string pullRequestNodeId; + Subject pullRequestChanged = new Subject(); + bool hasPendingReview; + + public PullRequestSession( + IPullRequestSessionService service, + ActorModel user, + PullRequestDetailModel pullRequest, + ILocalRepositoryModel localRepository, + string repositoryOwner, + bool isCheckedOut) + { + Guard.ArgumentNotNull(service, nameof(service)); + Guard.ArgumentNotNull(user, nameof(user)); + Guard.ArgumentNotNull(pullRequest, nameof(pullRequest)); + Guard.ArgumentNotNull(localRepository, nameof(localRepository)); + + this.service = service; + this.isCheckedOut = isCheckedOut; + this.pullRequest = pullRequest; + User = user; + LocalRepository = localRepository; + RepositoryOwner = repositoryOwner; + UpdatePendingReview(); + } + + /// + public async Task> GetAllFiles() + { + if (files == null) + { + files = await CreateAllFiles(); + } + + return files; + } + + /// + public async Task GetFile( + string relativePath, + string commitSha = "HEAD") + { + await getFilesLock.WaitAsync(); + + try + { + PullRequestSessionFile file; + var normalizedPath = relativePath.Replace("\\", "/"); + var key = normalizedPath + '@' + commitSha; + + if (!fileIndex.TryGetValue(key, out file)) + { + file = new PullRequestSessionFile(normalizedPath, commitSha); + await UpdateFile(file); + fileIndex.Add(key, file); + } + + return file; + } + finally + { + getFilesLock.Release(); + } + } + + /// + public async Task GetMergeBase() + { + if (mergeBase == null) + { + mergeBase = await service.GetPullRequestMergeBase(LocalRepository, PullRequest); + } + + return mergeBase; + } + + /// + public string GetRelativePath(string path) + { + if (Path.IsPathRooted(path)) + { + var basePath = LocalRepository.LocalPath; + + if (path.StartsWith(basePath, StringComparison.OrdinalIgnoreCase) && path.Length > basePath.Length + 1) + { + return path.Substring(basePath.Length + 1); + } + } + + return null; + } + + /// + public async Task PostReviewComment( + string body, + string commitId, + string path, + IReadOnlyList diff, + int position) + { + if (!HasPendingReview) + { + var model = await service.PostStandaloneReviewComment( + LocalRepository, + PullRequest.Id, + body, + commitId, + path, + position); + await Update(model); + } + else + { + var model = await service.PostPendingReviewComment( + LocalRepository, + PendingReviewId, + body, + commitId, + path, + position); + await Update(model); + } + } + + /// + public async Task DeleteComment(int pullRequestId, int commentDatabaseId) + { + var model = await service.DeleteComment( + LocalRepository, + RepositoryOwner, + pullRequestId, + commentDatabaseId); + + await Update(model); + } + + /// + public async Task EditComment(string commentNodeId, string body) + { + var model = await service.EditComment( + LocalRepository, + RepositoryOwner, + commentNodeId, + body); + + await Update(model); + } + + /// + public async Task PostReviewComment( + string body, + string inReplyTo) + { + if (!HasPendingReview) + { + var model = await service.PostStandaloneReviewCommentReply( + LocalRepository, + PullRequest.Id, + body, + inReplyTo); + await Update(model); + } + else + { + var model = await service.PostPendingReviewCommentReply( + LocalRepository, + PendingReviewId, + body, + inReplyTo); + await Update(model); + } + } + + /// + public async Task StartReview() + { + if (HasPendingReview) + { + throw new InvalidOperationException("A pending review is already underway."); + } + + var model = await service.CreatePendingReview( + LocalRepository, + await GetPullRequestNodeId()); + + await Update(model); + } + + /// + public async Task CancelReview() + { + if (!HasPendingReview) + { + throw new InvalidOperationException("There is no pending review to cancel."); + } + + var pullRequest = await service.CancelPendingReview(LocalRepository, PendingReviewId); + await Update(pullRequest); + } + + /// + public async Task PostReview(string body, Octokit.PullRequestReviewEvent e) + { + PullRequestDetailModel model; + + if (PendingReviewId == null) + { + model = await service.PostReview( + LocalRepository, + PullRequest.Id, + PullRequest.HeadRefSha, + body, + e); + } + else + { + model = await service.SubmitPendingReview( + LocalRepository, + PendingReviewId, + body, + e); + } + + await Update(model); + } + + /// + public async Task Refresh() + { + var address = HostAddress.Create(LocalRepository.CloneUrl); + var model = await service.ReadPullRequestDetail( + address, + RepositoryOwner, + LocalRepository.Name, + PullRequest.Number); + await Update(model); + } + + /// + async Task Update(PullRequestDetailModel pullRequestModel) + { + PullRequest = pullRequestModel; + mergeBase = null; + + foreach (var file in this.fileIndex.Values.ToList()) + { + await UpdateFile(file); + } + + UpdatePendingReview(); + pullRequestChanged.OnNext(pullRequestModel); + } + + async Task AddComment(PullRequestReviewCommentModel comment) + { + var review = PullRequest.Reviews.FirstOrDefault(x => x.Id == PendingReviewId); + + if (review == null) + { + throw new KeyNotFoundException("Could not find pending review."); + } + + review.Comments = review.Comments + .Concat(new[] { comment }) + .ToList(); + await Update(PullRequest); + } + + async Task UpdateFile(PullRequestSessionFile file) + { + await Task.Delay(0); + var mergeBaseSha = await GetMergeBase(); + file.BaseSha = PullRequest.BaseRefSha; + file.CommitSha = file.IsTrackingHead ? PullRequest.HeadRefSha : file.CommitSha; + file.Diff = await service.Diff(LocalRepository, mergeBaseSha, file.CommitSha, file.RelativePath); + file.InlineCommentThreads = service.BuildCommentThreads(PullRequest, file.RelativePath, file.Diff, file.CommitSha); + } + + void UpdatePendingReview() + { + var pendingReview = PullRequest.Reviews + .FirstOrDefault(x => x.State == PullRequestReviewState.Pending && x.Author.Login == User.Login); + + if (pendingReview != null) + { + HasPendingReview = true; + PendingReviewId = pendingReview.Id; + } + else + { + HasPendingReview = false; + PendingReviewId = null; + } + } + + async Task> CreateAllFiles() + { + var result = new List(); + + foreach (var path in FilePaths) + { + var file = await GetFile(path); + result.Add(file); + } + + return result; + } + + string GetFullPath(string relativePath) + { + return Path.Combine(LocalRepository.LocalPath, relativePath); + } + + async Task GetPullRequestNodeId() + { + if (pullRequestNodeId == null) + { + pullRequestNodeId = await service.GetGraphQLPullRequestId( + LocalRepository, + RepositoryOwner, + PullRequest.Number); + } + + return pullRequestNodeId; + } + + static string BuildDiffHunk(IReadOnlyList diff, int position) + { + var lines = diff.SelectMany(x => x.Lines).Reverse(); + var context = lines.SkipWhile(x => x.DiffLineNumber != position).Take(5).Reverse().ToList(); + var oldLineNumber = context.Select(x => x.OldLineNumber).Where(x => x != -1).FirstOrDefault(); + var newLineNumber = context.Select(x => x.NewLineNumber).Where(x => x != -1).FirstOrDefault(); + var header = Invariant($"@@ -{oldLineNumber},5 +{newLineNumber},5 @@"); + return header + '\n' + string.Join("\n", context); + } + + /// + public bool IsCheckedOut + { + get { return isCheckedOut; } + internal set { this.RaiseAndSetIfChanged(ref isCheckedOut, value); } + } + + /// + public ActorModel User { get; } + + /// + public PullRequestDetailModel PullRequest + { + get { return pullRequest; } + private set + { + // PullRequestModel overrides Equals such that two PRs with the same number are + // considered equal. This was causing the PullRequest not to be updated on refresh: + // we need to use ReferenceEquals. + if (!ReferenceEquals(pullRequest, value)) + { + this.RaisePropertyChanging(nameof(PullRequest)); + pullRequest = value; + this.RaisePropertyChanged(nameof(PullRequest)); + } + } + } + + /// + public IObservable PullRequestChanged => pullRequestChanged; + + /// + public ILocalRepositoryModel LocalRepository { get; } + + /// + public string RepositoryOwner { get; } + + /// + public bool HasPendingReview + { + get { return hasPendingReview; } + private set { this.RaiseAndSetIfChanged(ref hasPendingReview, value); } + } + + /// + public string PendingReviewId { get; private set; } + + IEnumerable FilePaths + { + get { return PullRequest.ChangedFiles.Select(x => x.FileName); } + } + } +} diff --git a/src/GitHub.InlineReviews/Services/PullRequestSessionManager.cs b/src/GitHub.InlineReviews/Services/PullRequestSessionManager.cs new file mode 100644 index 0000000000..b0d69c0266 --- /dev/null +++ b/src/GitHub.InlineReviews/Services/PullRequestSessionManager.cs @@ -0,0 +1,414 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading.Tasks; +using GitHub.Extensions; +using GitHub.Factories; +using GitHub.InlineReviews.Models; +using GitHub.Logging; +using GitHub.Models; +using GitHub.Primitives; +using GitHub.Services; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Projection; +using ReactiveUI; +using Serilog; + +namespace GitHub.InlineReviews.Services +{ + /// + /// Manages pull request sessions. + /// + [Export(typeof(IPullRequestSessionManager))] + [PartCreationPolicy(CreationPolicy.Shared)] + public class PullRequestSessionManager : ReactiveObject, IPullRequestSessionManager + { + static readonly ILogger log = LogManager.ForContext(); + readonly IPullRequestService service; + readonly IPullRequestSessionService sessionService; + readonly Dictionary, WeakReference> sessions = + new Dictionary, WeakReference>(); + TaskCompletionSource initialized; + IPullRequestSession currentSession; + ILocalRepositoryModel repository; + + /// + /// Initializes a new instance of the class. + /// + /// The PR service to use. + /// The PR session service to use. + /// The team explorer context to use. + [ImportingConstructor] + public PullRequestSessionManager( + IPullRequestService service, + IPullRequestSessionService sessionService, + ITeamExplorerContext teamExplorerContext) + { + Guard.ArgumentNotNull(service, nameof(service)); + Guard.ArgumentNotNull(sessionService, nameof(sessionService)); + Guard.ArgumentNotNull(teamExplorerContext, nameof(teamExplorerContext)); + + this.service = service; + this.sessionService = sessionService; + initialized = new TaskCompletionSource(null); + + Observable.FromEventPattern(teamExplorerContext, nameof(teamExplorerContext.StatusChanged)) + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(_ => StatusChanged().Forget(log)); + + teamExplorerContext.WhenAnyValue(x => x.ActiveRepository) + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(x => RepoChanged(x).Forget(log)); + } + + /// + public IPullRequestSession CurrentSession + { + get { return currentSession; } + private set { this.RaiseAndSetIfChanged(ref currentSession, value); } + } + + /// + public Task EnsureInitialized() => initialized.Task; + + /// + public async Task GetLiveFile( + string relativePath, + ITextView textView, + ITextBuffer textBuffer) + { + PullRequestSessionLiveFile result; + + if (!textBuffer.Properties.TryGetProperty( + typeof(IPullRequestSessionFile), + out result)) + { + var dispose = new CompositeDisposable(); + + result = new PullRequestSessionLiveFile( + relativePath, + textBuffer, + sessionService.CreateRebuildSignal()); + + textBuffer.Properties.AddProperty( + typeof(IPullRequestSessionFile), + result); + + await UpdateLiveFile(result, true); + + textBuffer.Changed += TextBufferChanged; + textView.Closed += TextViewClosed; + + dispose.Add(Disposable.Create(() => + { + textView.TextBuffer.Changed -= TextBufferChanged; + textView.Closed -= TextViewClosed; + })); + + dispose.Add(result.Rebuild.Subscribe(x => UpdateLiveFile(result, x).Forget())); + + dispose.Add(this.WhenAnyValue(x => x.CurrentSession) + .Skip(1) + .Subscribe(_ => UpdateLiveFile(result, true).Forget())); + dispose.Add(this.WhenAnyObservable(x => x.CurrentSession.PullRequestChanged) + .Subscribe(_ => UpdateLiveFile(result, true).Forget())); + + result.ToDispose = dispose; + } + + return result; + } + + /// + public string GetRelativePath(ITextBuffer buffer) + { + var document = sessionService.GetDocument(buffer); + var path = document?.FilePath; + + if (!string.IsNullOrWhiteSpace(path) && Path.IsPathRooted(path) && repository != null) + { + var basePath = repository.LocalPath; + + if (path.StartsWith(basePath, StringComparison.OrdinalIgnoreCase) && path.Length > basePath.Length + 1) + { + return path.Substring(basePath.Length + 1); + } + } + + return null; + } + + /// + public async Task GetSession(string owner, string name, int number) + { + var session = await GetSessionInternal(owner, name, number); + + if (await service.EnsureLocalBranchesAreMarkedAsPullRequests(repository, session.PullRequest)) + { + // The branch for the PR was not previously marked with the PR number in the git + // config so we didn't pick up that the current branch is a PR branch. That has + // now been corrected, so call StatusChanged to make sure everything is up-to-date. + await StatusChanged(); + } + + return session; + } + + /// + public PullRequestTextBufferInfo GetTextBufferInfo(ITextBuffer buffer) + { + var projectionBuffer = buffer as IProjectionBuffer; + PullRequestTextBufferInfo result; + + if (buffer.Properties.TryGetProperty(typeof(PullRequestTextBufferInfo), out result)) + { + return result; + } + + if (projectionBuffer != null) + { + foreach (var sourceBuffer in projectionBuffer.SourceBuffers) + { + var sourceBufferInfo = GetTextBufferInfo(sourceBuffer); + if (sourceBufferInfo != null) return sourceBufferInfo; + } + } + + return null; + } + + async Task RepoChanged(ILocalRepositoryModel localRepositoryModel) + { + repository = localRepositoryModel; + CurrentSession = null; + sessions.Clear(); + + if (localRepositoryModel != null) + { + await StatusChanged(); + } + } + + async Task StatusChanged() + { + var session = CurrentSession; + + var pr = await service.GetPullRequestForCurrentBranch(repository).FirstOrDefaultAsync(); + if (pr != null) + { + var changePR = + pr.Item1 != (session?.PullRequest.BaseRepositoryOwner) || + pr.Item2 != (session?.PullRequest.Number); + + if (changePR) + { + var newSession = await GetSessionInternal(pr.Item1, repository.Name, pr.Item2); + if (newSession != null) newSession.IsCheckedOut = true; + session = newSession; + } + } + else + { + session = null; + } + + CurrentSession = session; + initialized.TrySetResult(null); + } + + async Task GetSessionInternal(string owner, string name, int number) + { + PullRequestSession session = null; + WeakReference weakSession; + var key = Tuple.Create(owner.ToLowerInvariant(), number); + + if (sessions.TryGetValue(key, out weakSession)) + { + weakSession.TryGetTarget(out session); + } + + if (session == null) + { + var address = HostAddress.Create(repository.CloneUrl); + var pullRequest = await sessionService.ReadPullRequestDetail(address, owner, name, number); + + session = new PullRequestSession( + sessionService, + await sessionService.ReadViewer(address), + pullRequest, + repository, + key.Item1, + false); + sessions[key] = new WeakReference(session); + } + + return session; + } + + async Task UpdateLiveFile(PullRequestSessionLiveFile file, bool rebuildThreads) + { + var session = CurrentSession; + + if (session != null) + { + var mergeBase = await session.GetMergeBase(); + var contents = sessionService.GetContents(file.TextBuffer); + file.BaseSha = session.PullRequest.BaseRefSha; + file.CommitSha = await CalculateCommitSha(session, file, contents); + file.Diff = await sessionService.Diff( + session.LocalRepository, + mergeBase, + session.PullRequest.HeadRefSha, + file.RelativePath, + contents); + + if (rebuildThreads) + { + file.InlineCommentThreads = sessionService.BuildCommentThreads( + session.PullRequest, + file.RelativePath, + file.Diff, + session.PullRequest.HeadRefSha); + } + else + { + var changedLines = sessionService.UpdateCommentThreads( + file.InlineCommentThreads, + file.Diff); + + if (changedLines.Count > 0) + { + file.NotifyLinesChanged(changedLines); + } + } + + file.TrackingPoints = BuildTrackingPoints( + file.TextBuffer.CurrentSnapshot, + file.InlineCommentThreads); + } + else + { + file.BaseSha = null; + file.CommitSha = null; + file.Diff = null; + file.InlineCommentThreads = null; + file.TrackingPoints = null; + } + } + + async Task UpdateLiveFile(PullRequestSessionLiveFile file, ITextSnapshot snapshot) + { + if (file.TextBuffer.CurrentSnapshot == snapshot) + { + await UpdateLiveFile(file, false); + } + } + + void InvalidateLiveThreads(PullRequestSessionLiveFile file, ITextSnapshot snapshot) + { + if (file.TrackingPoints != null) + { + var linesChanged = new List>(); + + foreach (var thread in file.InlineCommentThreads) + { + ITrackingPoint trackingPoint; + + if (file.TrackingPoints.TryGetValue(thread, out trackingPoint)) + { + var position = trackingPoint.GetPosition(snapshot); + var lineNumber = snapshot.GetLineNumberFromPosition(position); + + if (thread.DiffLineType != DiffChangeType.Delete && lineNumber != thread.LineNumber) + { + linesChanged.Add(Tuple.Create(lineNumber, DiffSide.Right)); + linesChanged.Add(Tuple.Create(thread.LineNumber, DiffSide.Right)); + thread.LineNumber = lineNumber; + thread.IsStale = true; + } + } + } + + linesChanged = linesChanged + .Where(x => x.Item1 >= 0) + .Distinct() + .ToList(); + + if (linesChanged.Count > 0) + { + file.NotifyLinesChanged(linesChanged); + } + } + } + + private IDictionary BuildTrackingPoints( + ITextSnapshot snapshot, + IReadOnlyList threads) + { + var result = new Dictionary(); + + foreach (var thread in threads) + { + if (thread.LineNumber >= 0 && thread.DiffLineType != DiffChangeType.Delete) + { + var line = snapshot.GetLineFromLineNumber(thread.LineNumber); + var p = snapshot.CreateTrackingPoint(line.Start, PointTrackingMode.Positive); + result.Add(thread, p); + } + } + + return result; + } + + async Task CalculateCommitSha( + IPullRequestSession session, + IPullRequestSessionFile file, + byte[] content) + { + var repo = session.LocalRepository; + return await sessionService.IsUnmodifiedAndPushed(repo, file.RelativePath, content) ? + await sessionService.GetTipSha(repo) : null; + } + + private void CloseLiveFiles(ITextBuffer textBuffer) + { + PullRequestSessionLiveFile file; + + if (textBuffer.Properties.TryGetProperty( + typeof(IPullRequestSessionFile), + out file)) + { + file.Dispose(); + } + + var projection = textBuffer as IProjectionBuffer; + + if (projection != null) + { + foreach (var source in projection.SourceBuffers) + { + CloseLiveFiles(source); + } + } + } + + void TextBufferChanged(object sender, TextContentChangedEventArgs e) + { + var textBuffer = (ITextBuffer)sender; + var file = textBuffer.Properties.GetProperty(typeof(IPullRequestSessionFile)); + InvalidateLiveThreads(file, e.After); + file.Rebuild.OnNext(textBuffer.CurrentSnapshot); + } + + void TextViewClosed(object sender, EventArgs e) + { + var textView = (ITextView)sender; + CloseLiveFiles(textView.TextBuffer); + } + } +} diff --git a/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs b/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs new file mode 100644 index 0000000000..c4ed289942 --- /dev/null +++ b/src/GitHub.InlineReviews/Services/PullRequestSessionService.cs @@ -0,0 +1,926 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Text; +using System.Threading.Tasks; +using GitHub.Api; +using GitHub.App.Services; +using GitHub.Factories; +using GitHub.InlineReviews.Models; +using GitHub.Models; +using GitHub.Primitives; +using GitHub.Logging; +using GitHub.Services; +using LibGit2Sharp; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Projection; +using Octokit; +using Octokit.GraphQL; +using Octokit.GraphQL.Core; +using Octokit.GraphQL.Model; +using ReactiveUI; +using Serilog; +using PullRequestReviewEvent = Octokit.PullRequestReviewEvent; +using static Octokit.GraphQL.Variable; +using CheckAnnotationLevel = GitHub.Models.CheckAnnotationLevel; +using CheckConclusionState = GitHub.Models.CheckConclusionState; +using CheckStatusState = GitHub.Models.CheckStatusState; +using DraftPullRequestReviewComment = Octokit.GraphQL.Model.DraftPullRequestReviewComment; +using FileMode = System.IO.FileMode; +using NotFoundException = LibGit2Sharp.NotFoundException; +using PullRequestReviewState = Octokit.GraphQL.Model.PullRequestReviewState; +using StatusState = GitHub.Models.StatusState; + +// GraphQL DatabaseId field are marked as deprecated, but we need them for interop with REST. +#pragma warning disable CS0618 + +namespace GitHub.InlineReviews.Services +{ + /// + /// Provides a common interface for services required by . + /// + [Export(typeof(IPullRequestSessionService))] + public class PullRequestSessionService : IPullRequestSessionService + { + static readonly ILogger log = LogManager.ForContext(); + static ICompiledQuery readPullRequest; + static ICompiledQuery> readCommitStatuses; + static ICompiledQuery> readCommitStatusesEnterprise; + static ICompiledQuery readViewer; + + readonly IGitService gitService; + readonly IGitClient gitClient; + readonly IDiffService diffService; + readonly IApiClientFactory apiClientFactory; + readonly IGraphQLClientFactory graphqlFactory; + readonly IUsageTracker usageTracker; + readonly IDictionary, string> mergeBaseCache; + + [ImportingConstructor] + public PullRequestSessionService( + IGitService gitService, + IGitClient gitClient, + IDiffService diffService, + IApiClientFactory apiClientFactory, + IGraphQLClientFactory graphqlFactory, + IUsageTracker usageTracker) + { + this.gitService = gitService; + this.gitClient = gitClient; + this.diffService = diffService; + this.apiClientFactory = apiClientFactory; + this.graphqlFactory = graphqlFactory; + this.usageTracker = usageTracker; + + mergeBaseCache = new Dictionary, string>(); + } + + /// + public virtual async Task> Diff(ILocalRepositoryModel repository, string baseSha, string headSha, string relativePath) + { + using (var repo = await GetRepository(repository)) + { + return await diffService.Diff(repo, baseSha, headSha, relativePath); + } + } + + /// + public virtual async Task> Diff(ILocalRepositoryModel repository, string baseSha, string headSha, string relativePath, byte[] contents) + { + using (var repo = await GetRepository(repository)) + { + return await diffService.Diff(repo, baseSha, headSha, relativePath, contents); + } + } + + /// + public IReadOnlyList BuildCommentThreads( + PullRequestDetailModel pullRequest, + string relativePath, + IReadOnlyList diff, + string headSha) + { + relativePath = relativePath.Replace("\\", "/"); + + var threadsByPosition = pullRequest.Threads + .Where(x => x.Path == relativePath && !x.IsOutdated) + .OrderBy(x => x.Id) + .GroupBy(x => Tuple.Create(x.OriginalCommitSha, x.OriginalPosition)); + var threads = new List(); + + foreach (var thread in threadsByPosition) + { + var hunk = thread.First().DiffHunk; + var chunks = DiffUtilities.ParseFragment(hunk); + var chunk = chunks.Last(); + var diffLines = chunk.Lines.Reverse().Take(5).ToList(); + var firstLine = diffLines.FirstOrDefault(); + if (firstLine == null) + { + log.Warning("Ignoring in-line comment in {RelativePath} with no diff line context", relativePath); + continue; + } + + var inlineThread = new InlineCommentThreadModel( + relativePath, + headSha, + diffLines, + thread.SelectMany(t => t.Comments.Select(c => new InlineCommentModel + { + Comment = c, + Review = pullRequest.Reviews.FirstOrDefault(x => x.Comments.Contains(c)), + }))); + threads.Add(inlineThread); + } + + UpdateCommentThreads(threads, diff); + return threads; + } + + /// + public IReadOnlyList> UpdateCommentThreads( + IReadOnlyList threads, + IReadOnlyList diff) + { + var changedLines = new List>(); + + foreach (var thread in threads) + { + var oldLineNumber = thread.LineNumber; + var newLineNumber = GetUpdatedLineNumber(thread, diff); + var changed = false; + + if (thread.IsStale) + { + thread.IsStale = false; + changed = true; + } + + if (newLineNumber != thread.LineNumber) + { + thread.LineNumber = newLineNumber; + thread.IsStale = false; + changed = true; + } + + if (changed) + { + var side = thread.DiffLineType == DiffChangeType.Delete ? GitHub.Models.DiffSide.Left : GitHub.Models.DiffSide.Right; + if (oldLineNumber != -1) changedLines.Add(Tuple.Create(oldLineNumber, side)); + if (newLineNumber != -1 && newLineNumber != oldLineNumber) changedLines.Add(Tuple.Create(newLineNumber, side)); + } + } + + return changedLines; + } + + /// + public byte[] GetContents(ITextBuffer buffer) + { + var encoding = GetDocument(buffer)?.Encoding ?? Encoding.Default; + var content = encoding.GetBytes(buffer.CurrentSnapshot.GetText()); + + var preamble = encoding.GetPreamble(); + if (preamble.Length == 0) return content; + + var completeContent = new byte[preamble.Length + content.Length]; + Buffer.BlockCopy(preamble, 0, completeContent, 0, preamble.Length); + Buffer.BlockCopy(content, 0, completeContent, preamble.Length, content.Length); + + return completeContent; + } + + /// + public ITextDocument GetDocument(ITextBuffer buffer) + { + ITextDocument result; + + if (buffer.Properties.TryGetProperty(typeof(ITextDocument), out result)) + return result; + + var projection = buffer as IProjectionBuffer; + + if (projection != null) + { + foreach (var source in projection.SourceBuffers) + { + if ((result = GetDocument(source)) != null) + return result; + } + } + + return null; + } + + /// + public virtual async Task GetTipSha(ILocalRepositoryModel repository) + { + using (var repo = await GetRepository(repository)) + { + return repo.Head.Tip.Sha; + } + } + + /// + public async Task IsUnmodifiedAndPushed(ILocalRepositoryModel repository, string relativePath, byte[] contents) + { + using (var repo = await GetRepository(repository)) + { + var modified = await gitClient.IsModified(repo, relativePath, contents); + var pushed = await gitClient.IsHeadPushed(repo); + + return !modified && pushed; + } + } + + public async Task ExtractFileFromGit( + ILocalRepositoryModel repository, + int pullRequestNumber, + string sha, + string relativePath) + { + using (var repo = await GetRepository(repository)) + { + try + { + return await gitClient.ExtractFileBinary(repo, sha, relativePath); + } + catch (FileNotFoundException) + { + var pullHeadRef = $"refs/pull/{pullRequestNumber}/head"; + await gitClient.Fetch(repo, "origin", sha, pullHeadRef); + return await gitClient.ExtractFileBinary(repo, sha, relativePath); + } + } + } + + /// + public async Task ReadFileAsync(string path) + { + if (File.Exists(path)) + { + try + { + using (var file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true)) + { + var buffer = new MemoryStream(); + await file.CopyToAsync(buffer); + return buffer.ToArray(); + } + } + catch { } + } + + return null; + } + + public virtual async Task ReadPullRequestDetail(HostAddress address, string owner, string name, int number) + { + if (readPullRequest == null) + { + readPullRequest = new Query() + .Repository(Var(nameof(owner)), Var(nameof(name))) + .PullRequest(Var(nameof(number))) + .Select(pr => new PullRequestDetailModel + { + Id = pr.Id.Value, + Number = pr.Number, + Author = new ActorModel + { + Login = pr.Author.Login, + AvatarUrl = pr.Author.AvatarUrl(null), + }, + Title = pr.Title, + Body = pr.Body, + BaseRefSha = pr.BaseRefOid, + BaseRefName = pr.BaseRefName, + BaseRepositoryOwner = pr.Repository.Owner.Login, + HeadRefName = pr.HeadRefName, + HeadRefSha = pr.HeadRefOid, + HeadRepositoryOwner = pr.HeadRepositoryOwner != null ? pr.HeadRepositoryOwner.Login : null, + State = pr.State.FromGraphQl(), + UpdatedAt = pr.UpdatedAt, + Reviews = pr.Reviews(null, null, null, null, null, null).AllPages().Select(review => new PullRequestReviewModel + { + Id = review.Id.Value, + Body = review.Body, + CommitId = review.Commit.Oid, + State = review.State.FromGraphQl(), + SubmittedAt = review.SubmittedAt, + Author = new ActorModel + { + Login = review.Author.Login, + AvatarUrl = review.Author.AvatarUrl(null), + }, + Comments = review.Comments(null, null, null, null).AllPages().Select(comment => new CommentAdapter + { + Id = comment.Id.Value, + PullRequestId = comment.PullRequest.Number, + DatabaseId = comment.DatabaseId.Value, + Author = new ActorModel + { + Login = comment.Author.Login, + AvatarUrl = comment.Author.AvatarUrl(null), + }, + Body = comment.Body, + Path = comment.Path, + CommitSha = comment.Commit.Oid, + DiffHunk = comment.DiffHunk, + Position = comment.Position, + OriginalPosition = comment.OriginalPosition, + OriginalCommitId = comment.OriginalCommit.Oid, + ReplyTo = comment.ReplyTo != null ? comment.ReplyTo.Id.Value : null, + CreatedAt = comment.CreatedAt, + Url = comment.Url, + }).ToList(), + }).ToList(), + }).Compile(); + } + + var vars = new Dictionary + { + { nameof(owner), owner }, + { nameof(name), name }, + { nameof(number), number }, + }; + + var connection = await graphqlFactory.CreateConnection(address); + var result = await connection.Run(readPullRequest, vars); + + var apiClient = await apiClientFactory.Create(address); + var files = await apiClient.GetPullRequestFiles(owner, name, number).ToList(); + var lastCommitModel = await GetPullRequestLastCommitAdapter(address, owner, name, number); + + result.Statuses = lastCommitModel.Statuses; + result.CheckSuites = lastCommitModel.CheckSuites; + + result.ChangedFiles = files.Select(file => new PullRequestFileModel + { + FileName = file.FileName, + Sha = file.Sha, + Status = (PullRequestFileStatus)Enum.Parse(typeof(PullRequestFileStatus), file.Status, true), + }).ToList(); + + BuildPullRequestThreads(result); + return result; + } + + public virtual async Task ReadViewer(HostAddress address) + { + if (readViewer == null) + { + readViewer = new Query() + .Viewer + .Select(x => new ActorModel + { + Login = x.Login, + AvatarUrl = x.AvatarUrl(null), + }).Compile(); + } + + var connection = await graphqlFactory.CreateConnection(address); + return await connection.Run(readViewer); + } + + public async Task GetGraphQLPullRequestId( + ILocalRepositoryModel localRepository, + string repositoryOwner, + int number) + { + var address = HostAddress.Create(localRepository.CloneUrl.Host); + var graphql = await graphqlFactory.CreateConnection(address); + + var query = new Query() + .Repository(repositoryOwner, localRepository.Name) + .PullRequest(number) + .Select(x => x.Id); + + return (await graphql.Run(query)).Value; + } + + /// + public virtual async Task GetPullRequestMergeBase(ILocalRepositoryModel repository, PullRequestDetailModel pullRequest) + { + var baseSha = pullRequest.BaseRefSha; + var headSha = pullRequest.HeadRefSha; + var key = new Tuple(baseSha, headSha); + + string mergeBase; + if (mergeBaseCache.TryGetValue(key, out mergeBase)) + { + return mergeBase; + } + + using (var repo = await GetRepository(repository)) + { + var targetUrl = repository.CloneUrl.WithOwner(pullRequest.BaseRepositoryOwner); + var headUrl = repository.CloneUrl.WithOwner(pullRequest.HeadRepositoryOwner); + var baseRef = pullRequest.BaseRefName; + var pullNumber = pullRequest.Number; + try + { + mergeBase = await gitClient.GetPullRequestMergeBase(repo, targetUrl, baseSha, headSha, baseRef, pullNumber); + } + catch (NotFoundException ex) + { + throw new NotFoundException("The Pull Request failed to load. Please check your network connection and click refresh to try again. If this issue persists, please let us know at support@github.com", ex); + } + + return mergeBaseCache[key] = mergeBase; + } + } + + /// + public virtual ISubject CreateRebuildSignal() + { + var input = new Subject(); + var output = Observable.Create(x => input + .Throttle(TimeSpan.FromMilliseconds(500)) + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(x)); + return Subject.Create(input, output); + } + + /// + public async Task CreatePendingReview( + ILocalRepositoryModel localRepository, + string pullRequestId) + { + var address = HostAddress.Create(localRepository.CloneUrl.Host); + var graphql = await graphqlFactory.CreateConnection(address); + var (_, owner, number) = await CreatePendingReviewCore(localRepository, pullRequestId); + var detail = await ReadPullRequestDetail(address, owner, localRepository.Name, number); + + await usageTracker.IncrementCounter(x => x.NumberOfPRReviewDiffViewInlineCommentStartReview); + + return detail; + } + + /// + public async Task CancelPendingReview( + ILocalRepositoryModel localRepository, + string reviewId) + { + var address = HostAddress.Create(localRepository.CloneUrl.Host); + var graphql = await graphqlFactory.CreateConnection(address); + + var delete = new DeletePullRequestReviewInput + { + PullRequestReviewId = new ID(reviewId), + }; + + var mutation = new Mutation() + .DeletePullRequestReview(delete) + .Select(x => new + { + x.PullRequestReview.Repository.Owner.Login, + x.PullRequestReview.PullRequest.Number + }); + + var result = await graphql.Run(mutation); + return await ReadPullRequestDetail(address, result.Login, localRepository.Name, result.Number); + } + + /// + public async Task PostReview( + ILocalRepositoryModel localRepository, + string pullRequestId, + string commitId, + string body, + PullRequestReviewEvent e) + { + var address = HostAddress.Create(localRepository.CloneUrl.Host); + var graphql = await graphqlFactory.CreateConnection(address); + + var addReview = new AddPullRequestReviewInput + { + Body = body, + CommitOID = commitId, + Event = ToGraphQl(e), + PullRequestId = new ID(pullRequestId), + }; + + var mutation = new Mutation() + .AddPullRequestReview(addReview) + .Select(x => new + { + x.PullRequestReview.Repository.Owner.Login, + x.PullRequestReview.PullRequest.Number + }); + + var result = await graphql.Run(mutation); + await usageTracker.IncrementCounter(x => x.NumberOfPRReviewPosts); + return await ReadPullRequestDetail(address, result.Login, localRepository.Name, result.Number); + } + + public async Task SubmitPendingReview( + ILocalRepositoryModel localRepository, + string pendingReviewId, + string body, + PullRequestReviewEvent e) + { + var address = HostAddress.Create(localRepository.CloneUrl.Host); + var graphql = await graphqlFactory.CreateConnection(address); + + var submit = new SubmitPullRequestReviewInput + { + Body = body, + Event = ToGraphQl(e), + PullRequestReviewId = new ID(pendingReviewId), + }; + + var mutation = new Mutation() + .SubmitPullRequestReview(submit) + .Select(x => new + { + x.PullRequestReview.Repository.Owner.Login, + x.PullRequestReview.PullRequest.Number + }); + + var result = await graphql.Run(mutation); + await usageTracker.IncrementCounter(x => x.NumberOfPRReviewPosts); + return await ReadPullRequestDetail(address, result.Login, localRepository.Name, result.Number); + } + + /// + public async Task PostPendingReviewComment( + ILocalRepositoryModel localRepository, + string pendingReviewId, + string body, + string commitId, + string path, + int position) + { + var address = HostAddress.Create(localRepository.CloneUrl.Host); + var graphql = await graphqlFactory.CreateConnection(address); + + var comment = new AddPullRequestReviewCommentInput + { + Body = body, + CommitOID = commitId, + Path = path, + Position = position, + PullRequestReviewId = new ID(pendingReviewId), + }; + + var addComment = new Mutation() + .AddPullRequestReviewComment(comment) + .Select(x => new + { + x.Comment.Repository.Owner.Login, + x.Comment.PullRequest.Number + }); + + var result = await graphql.Run(addComment); + await usageTracker.IncrementCounter(x => x.NumberOfPRReviewDiffViewInlineCommentPost); + return await ReadPullRequestDetail(address, result.Login, localRepository.Name, result.Number); + } + + /// + public async Task PostPendingReviewCommentReply( + ILocalRepositoryModel localRepository, + string pendingReviewId, + string body, + string inReplyTo) + { + var address = HostAddress.Create(localRepository.CloneUrl.Host); + var graphql = await graphqlFactory.CreateConnection(address); + + var comment = new AddPullRequestReviewCommentInput + { + Body = body, + InReplyTo = new ID(inReplyTo), + PullRequestReviewId = new ID(pendingReviewId), + }; + + var addComment = new Mutation() + .AddPullRequestReviewComment(comment) + .Select(x => new + { + x.Comment.Repository.Owner.Login, + x.Comment.PullRequest.Number + }); + + var result = await graphql.Run(addComment); + await usageTracker.IncrementCounter(x => x.NumberOfPRReviewDiffViewInlineCommentPost); + return await ReadPullRequestDetail(address, result.Login, localRepository.Name, result.Number); + } + + /// + public async Task PostStandaloneReviewComment( + ILocalRepositoryModel localRepository, + string pullRequestId, + string body, + string commitId, + string path, + int position) + { + var address = HostAddress.Create(localRepository.CloneUrl.Host); + var graphql = await graphqlFactory.CreateConnection(address); + + var addReview = new AddPullRequestReviewInput + { + Body = body, + CommitOID = commitId, + Event = Octokit.GraphQL.Model.PullRequestReviewEvent.Comment, + PullRequestId = new ID(pullRequestId), + Comments = new[] + { + new DraftPullRequestReviewComment + { + Body = body, + Path = path, + Position = position, + }, + }, + }; + + var mutation = new Mutation() + .AddPullRequestReview(addReview) + .Select(x => new + { + x.PullRequestReview.Repository.Owner.Login, + x.PullRequestReview.PullRequest.Number + }); + + var result = await graphql.Run(mutation); + await usageTracker.IncrementCounter(x => x.NumberOfPRReviewDiffViewInlineCommentPost); + return await ReadPullRequestDetail(address, result.Login, localRepository.Name, result.Number); + } + + /// + public async Task PostStandaloneReviewCommentReply( + ILocalRepositoryModel localRepository, + string pullRequestId, + string body, + string inReplyTo) + { + var (id, _, _) = await CreatePendingReviewCore(localRepository, pullRequestId); + var comment = await PostPendingReviewCommentReply(localRepository, id, body, inReplyTo); + return await SubmitPendingReview(localRepository, id, null, PullRequestReviewEvent.Comment); + } + + /// + public async Task DeleteComment( + ILocalRepositoryModel localRepository, + string remoteRepositoryOwner, + int pullRequestId, + int commentDatabaseId) + { + var address = HostAddress.Create(localRepository.CloneUrl.Host); + var apiClient = await apiClientFactory.Create(address); + + await apiClient.DeletePullRequestReviewComment( + remoteRepositoryOwner, + localRepository.Name, + commentDatabaseId); + + await usageTracker.IncrementCounter(x => x.NumberOfPRReviewDiffViewInlineCommentDelete); + return await ReadPullRequestDetail(address, remoteRepositoryOwner, localRepository.Name, pullRequestId); + } + + /// + public async Task EditComment(ILocalRepositoryModel localRepository, + string remoteRepositoryOwner, + string commentNodeId, + string body) + { + var address = HostAddress.Create(localRepository.CloneUrl.Host); + var graphql = await graphqlFactory.CreateConnection(address); + + var updatePullRequestReviewCommentInput = new UpdatePullRequestReviewCommentInput + { + Body = body, + PullRequestReviewCommentId = new ID(commentNodeId), + }; + + var editComment = new Mutation().UpdatePullRequestReviewComment(updatePullRequestReviewCommentInput) + .Select(x => new + { + x.PullRequestReviewComment.Repository.Owner.Login, + x.PullRequestReviewComment.PullRequest.Number + }); + + var result = await graphql.Run(editComment); + await usageTracker.IncrementCounter(x => x.NumberOfPRReviewDiffViewInlineCommentPost); + return await ReadPullRequestDetail(address, result.Login, localRepository.Name, result.Number); + } + + async Task<(string id, string owner, int number)> CreatePendingReviewCore(ILocalRepositoryModel localRepository, string pullRequestId) + { + var address = HostAddress.Create(localRepository.CloneUrl.Host); + var graphql = await graphqlFactory.CreateConnection(address); + + var input = new AddPullRequestReviewInput + { + PullRequestId = new ID(pullRequestId), + }; + + var mutation = new Mutation() + .AddPullRequestReview(input) + .Select(x => new + { + Id = x.PullRequestReview.Id.Value, + Owner = x.PullRequestReview.Repository.Owner.Login, + x.PullRequestReview.PullRequest.Number + }); + + var result = await graphql.Run(mutation); + return (result.Id, result.Owner, result.Number); + } + + int GetUpdatedLineNumber(IInlineCommentThreadModel thread, IEnumerable diff) + { + var line = DiffUtilities.Match(diff, thread.DiffMatch); + + if (line != null) + { + return (thread.DiffLineType == DiffChangeType.Delete) ? + line.OldLineNumber - 1 : + line.NewLineNumber - 1; + } + + return -1; + } + + Task GetRepository(ILocalRepositoryModel repository) + { + return Task.Factory.StartNew(() => gitService.GetRepository(repository.LocalPath)); + } + + async Task GetPullRequestLastCommitAdapter(HostAddress address, string owner, string name, int number) + { + ICompiledQuery> query; + if (address.IsGitHubDotCom()) + { + if (readCommitStatuses == null) + { + readCommitStatuses = new Query() + .Repository(Var(nameof(owner)), Var(nameof(name))) + .PullRequest(Var(nameof(number))).Commits(last: 1).Nodes.Select( + commit => new LastCommitAdapter + { + CheckSuites = commit.Commit.CheckSuites(null, null, null, null, null).AllPages(10) + .Select(suite => new CheckSuiteModel + { + CheckRuns = suite.CheckRuns(null, null, null, null, null).AllPages(10) + .Select(run => new CheckRunModel + { + Conclusion = run.Conclusion.FromGraphQl(), + Status = run.Status.FromGraphQl(), + Name = run.Name, + DetailsUrl = run.Permalink, + Summary = run.Summary, + }).ToList() + }).ToList(), + Statuses = commit.Commit.Status + .Select(context => + context.Contexts.Select(statusContext => new StatusModel + { + State = statusContext.State.FromGraphQl(), + Context = statusContext.Context, + TargetUrl = statusContext.TargetUrl, + Description = statusContext.Description, + }).ToList() + ).SingleOrDefault() + } + ).Compile(); + } + + query = readCommitStatuses; + } + else + { + if (readCommitStatusesEnterprise == null) + { + readCommitStatusesEnterprise = new Query() + .Repository(Var(nameof(owner)), Var(nameof(name))) + .PullRequest(Var(nameof(number))).Commits(last: 1).Nodes.Select( + commit => new LastCommitAdapter + { + Statuses = commit.Commit.Status + .Select(context => + context.Contexts.Select(statusContext => new StatusModel + { + State = statusContext.State.FromGraphQl(), + Context = statusContext.Context, + TargetUrl = statusContext.TargetUrl, + Description = statusContext.Description, + }).ToList() + ).SingleOrDefault() + } + ).Compile(); + } + + query = readCommitStatusesEnterprise; + } + + var vars = new Dictionary + { + { nameof(owner), owner }, + { nameof(name), name }, + { nameof(number), number }, + }; + + var connection = await graphqlFactory.CreateConnection(address); + var result = await connection.Run(query, vars); + return result.First(); + } + + static void BuildPullRequestThreads(PullRequestDetailModel model) + { + var commentsByReplyId = new Dictionary>(); + + // Get all comments that are not replies. + foreach (CommentAdapter comment in model.Reviews.SelectMany(x => x.Comments)) + { + if (comment.ReplyTo == null) + { + commentsByReplyId.Add(comment.Id, new List { comment }); + } + } + + // Get the comments that are replies and place them into the relevant list. + foreach (CommentAdapter comment in model.Reviews.SelectMany(x => x.Comments)) + { + if (comment.ReplyTo != null) + { + List thread = null; + + if (commentsByReplyId.TryGetValue(comment.ReplyTo, out thread)) + { + thread.Add(comment); + } + } + } + + // Build a collection of threads for the information collected above. + var threads = new List(); + + foreach (var threadSource in commentsByReplyId) + { + var adapter = threadSource.Value[0]; + + var thread = new PullRequestReviewThreadModel + { + Comments = threadSource.Value, + CommitSha = adapter.CommitSha, + DiffHunk = adapter.DiffHunk, + Id = adapter.Id, + IsOutdated = adapter.Position == null, + OriginalCommitSha = adapter.OriginalCommitId, + OriginalPosition = adapter.OriginalPosition, + Path = adapter.Path, + Position = adapter.Position, + }; + + // Set a reference to the thread in the comment. + foreach (var comment in threadSource.Value) + { + comment.Thread = thread; + } + + threads.Add(thread); + } + + model.Threads = threads; + } + + static Octokit.GraphQL.Model.PullRequestReviewEvent ToGraphQl(Octokit.PullRequestReviewEvent e) + { + switch (e) + { + case Octokit.PullRequestReviewEvent.Approve: + return Octokit.GraphQL.Model.PullRequestReviewEvent.Approve; + case Octokit.PullRequestReviewEvent.Comment: + return Octokit.GraphQL.Model.PullRequestReviewEvent.Comment; + case Octokit.PullRequestReviewEvent.RequestChanges: + return Octokit.GraphQL.Model.PullRequestReviewEvent.RequestChanges; + default: + throw new NotSupportedException(); + } + } + + class CommentAdapter : PullRequestReviewCommentModel + { + public string Path { get; set; } + public string CommitSha { get; set; } + public string DiffHunk { get; set; } + public int? Position { get; set; } + public int OriginalPosition { get; set; } + public string OriginalCommitId { get; set; } + public string ReplyTo { get; set; } + } + + class LastCommitAdapter + { + public List CheckSuites { get; set; } + + public List Statuses { get; set; } + } + } +} diff --git a/src/GitHub.InlineReviews/Services/PullRequestStatusBarManager.cs b/src/GitHub.InlineReviews/Services/PullRequestStatusBarManager.cs new file mode 100644 index 0000000000..c6672b2847 --- /dev/null +++ b/src/GitHub.InlineReviews/Services/PullRequestStatusBarManager.cs @@ -0,0 +1,182 @@ +using System; +using System.Windows; +using System.Windows.Input; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.ComponentModel.Composition; +using System.Reactive.Linq; +using System.Threading.Tasks; +using GitHub.Commands; +using GitHub.Extensions; +using GitHub.Primitives; +using GitHub.InlineReviews.Views; +using GitHub.InlineReviews.ViewModels; +using GitHub.Services; +using GitHub.Models; +using GitHub.Logging; +using Serilog; +using ReactiveUI; + +namespace GitHub.InlineReviews.Services +{ + /// + /// Manage the UI that shows the PR for the current branch. + /// + [Export(typeof(PullRequestStatusBarManager))] + public class PullRequestStatusBarManager + { + static readonly ILogger log = LogManager.ForContext(); + const string StatusBarPartName = "PART_SccStatusBarHost"; + + readonly ICommand openPullRequestsCommand; + readonly ICommand showCurrentPullRequestCommand; + + // At the moment these must be constructed on the main thread. + // TeamExplorerContext needs to retrieve DTE using GetService. + readonly Lazy pullRequestSessionManager; + readonly Lazy teamExplorerContext; + readonly Lazy connectionManager; + + IDisposable currentSessionSubscription; + + [ImportingConstructor] + public PullRequestStatusBarManager( + Lazy usageTracker, + IOpenPullRequestsCommand openPullRequestsCommand, + IShowCurrentPullRequestCommand showCurrentPullRequestCommand, + Lazy pullRequestSessionManager, + Lazy teamExplorerContext, + Lazy connectionManager) + { + this.openPullRequestsCommand = new UsageTrackingCommand(usageTracker, + x => x.NumberOfStatusBarOpenPullRequestList, openPullRequestsCommand); + this.showCurrentPullRequestCommand = new UsageTrackingCommand(usageTracker, + x => x.NumberOfShowCurrentPullRequest, showCurrentPullRequestCommand); + + this.pullRequestSessionManager = pullRequestSessionManager; + this.teamExplorerContext = teamExplorerContext; + this.connectionManager = connectionManager; + } + + /// + /// Start showing the PR for the active branch on the status bar. + /// + /// + /// This must be called from the Main thread. + /// + public void StartShowingStatus() + { + try + { + teamExplorerContext.Value.WhenAnyValue(x => x.ActiveRepository) + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(x => RefreshActiveRepository(x)); + } + catch (Exception e) + { + log.Error(e, "Error initializing"); + } + } + + void RefreshActiveRepository(ILocalRepositoryModel repository) + { + currentSessionSubscription?.Dispose(); + currentSessionSubscription = pullRequestSessionManager.Value.WhenAnyValue(x => x.CurrentSession) + .Subscribe(x => RefreshCurrentSession(repository, x).Forget()); + } + + async Task RefreshCurrentSession(ILocalRepositoryModel repository, IPullRequestSession session) + { + try + { + var showStatus = await IsDotComOrEnterpriseRepository(repository); + if (!showStatus) + { + ShowStatus(null); + return; + } + + var viewModel = CreatePullRequestStatusViewModel(session); + ShowStatus(viewModel); + } + catch (Exception e) + { + log.Error(e, nameof(RefreshCurrentSession)); + } + } + + async Task IsDotComOrEnterpriseRepository(ILocalRepositoryModel repository) + { + var cloneUrl = repository?.CloneUrl; + if (cloneUrl == null) + { + // No active repository or remote + return false; + } + + var isDotCom = HostAddress.IsGitHubDotComUri(cloneUrl.ToRepositoryUrl()); + if (isDotCom) + { + // This is a github.com repository + return true; + } + + var connection = await connectionManager.Value.GetConnection(repository); + if (connection != null) + { + // This is an enterprise repository + return true; + } + + return false; + } + + PullRequestStatusViewModel CreatePullRequestStatusViewModel(IPullRequestSession session) + { + var pullRequestStatusViewModel = new PullRequestStatusViewModel(openPullRequestsCommand, showCurrentPullRequestCommand); + var pullRequest = session?.PullRequest; + 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; + } + } +} diff --git a/src/GitHub.InlineReviews/Tags/AddInlineCommentGlyph.xaml b/src/GitHub.InlineReviews/Tags/AddInlineCommentGlyph.xaml new file mode 100644 index 0000000000..bc2a7cff7b --- /dev/null +++ b/src/GitHub.InlineReviews/Tags/AddInlineCommentGlyph.xaml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/src/GitHub.InlineReviews/Tags/AddInlineCommentGlyph.xaml.cs b/src/GitHub.InlineReviews/Tags/AddInlineCommentGlyph.xaml.cs new file mode 100644 index 0000000000..f3d7bc33cc --- /dev/null +++ b/src/GitHub.InlineReviews/Tags/AddInlineCommentGlyph.xaml.cs @@ -0,0 +1,18 @@ +using System; +using System.Windows; +using System.Windows.Controls; + +namespace GitHub.InlineReviews.Tags +{ + public partial class AddInlineCommentGlyph : UserControl + { + public AddInlineCommentGlyph() + { + InitializeComponent(); + + AddViewbox.Visibility = Visibility.Hidden; + MouseEnter += (s, e) => AddViewbox.Visibility = Visibility.Visible; + MouseLeave += (s, e) => AddViewbox.Visibility = Visibility.Hidden; + } + } +} diff --git a/src/GitHub.InlineReviews/Tags/AddInlineCommentTag.cs b/src/GitHub.InlineReviews/Tags/AddInlineCommentTag.cs new file mode 100644 index 0000000000..2e64f6ebf8 --- /dev/null +++ b/src/GitHub.InlineReviews/Tags/AddInlineCommentTag.cs @@ -0,0 +1,57 @@ +using System; +using GitHub.Services; +using GitHub.Models; + +namespace GitHub.InlineReviews.Tags +{ + /// + /// A tag which marks a line in an editor where a new review comment can be added. + /// + public class AddInlineCommentTag : InlineCommentTag + { + /// + /// Initializes a new instance of the class. + /// + /// The pull request session. + /// + /// The SHA of the commit to which a new comment should be added. May be null if the tag + /// represents trying to add a comment to a line that hasn't yet been pushed. + /// + /// The path to the file. + /// The line in the diff that the line relates to. + /// The line in the file. + /// The type of represented by the diff line. + public AddInlineCommentTag( + IPullRequestSession session, + string commitSha, + string filePath, + int diffLine, + int lineNumber, + DiffChangeType diffChangeType) + : base(session, lineNumber, diffChangeType) + { + CommitSha = commitSha; + DiffLine = diffLine; + FilePath = filePath; + } + + /// + /// Gets the SHA of the commit to which a new comment should be added. + /// + /// + /// May be null if the tag represents trying to add a comment to a line that hasn't yet been + /// pushed. + /// + public string CommitSha { get; } + + /// + /// Gets the line in the diff that the line relates to. + /// + public int DiffLine { get; } + + /// + /// Gets the path to the file. + /// + public string FilePath { get; } + } +} diff --git a/src/GitHub.InlineReviews/Tags/InlineCommentGlyphFactory.cs b/src/GitHub.InlineReviews/Tags/InlineCommentGlyphFactory.cs new file mode 100644 index 0000000000..38b62be7d3 --- /dev/null +++ b/src/GitHub.InlineReviews/Tags/InlineCommentGlyphFactory.cs @@ -0,0 +1,87 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using GitHub.InlineReviews.Glyph; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Formatting; +using GitHub.InlineReviews.Services; + +namespace GitHub.InlineReviews.Tags +{ + class InlineCommentGlyphFactory : IGlyphFactory + { + readonly IInlineCommentPeekService peekService; + readonly ITextView textView; + + public InlineCommentGlyphFactory( + IInlineCommentPeekService peekService, + ITextView textView) + { + this.peekService = peekService; + this.textView = textView; + } + + public UIElement GenerateGlyph(IWpfTextViewLine line, InlineCommentTag tag) + { + var glyph = CreateGlyph(tag); + glyph.DataContext = tag; + glyph.MouseLeftButtonUp += (s, e) => + { + if (OpenThreadView(tag)) e.Handled = true; + }; + + return glyph; + } + + public IEnumerable GetTagTypes() + { + return new[] + { + typeof(AddInlineCommentTag), + typeof(ShowInlineCommentTag) + }; + } + + [SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object)")] + static UserControl CreateGlyph(InlineCommentTag tag) + { + var addTag = tag as AddInlineCommentTag; + var showTag = tag as ShowInlineCommentTag; + + if (addTag != null) + { + return new AddInlineCommentGlyph(); + } + else if (showTag != null) + { + return new ShowInlineCommentGlyph() + { + Opacity = showTag.Thread.IsStale ? 0.5 : 1, + }; + } + + throw new ArgumentException($"Unknown 'InlineCommentTag' type '{tag}'"); + } + + bool OpenThreadView(InlineCommentTag tag) + { + var addTag = tag as AddInlineCommentTag; + var showTag = tag as ShowInlineCommentTag; + + if (addTag != null) + { + peekService.Show(textView, addTag); + return true; + } + else if (showTag != null) + { + peekService.Show(textView, showTag); + return true; + } + + return false; + } + } +} diff --git a/src/GitHub.InlineReviews/Tags/InlineCommentTag.cs b/src/GitHub.InlineReviews/Tags/InlineCommentTag.cs new file mode 100644 index 0000000000..bdf46a062e --- /dev/null +++ b/src/GitHub.InlineReviews/Tags/InlineCommentTag.cs @@ -0,0 +1,31 @@ +using GitHub.Extensions; +using GitHub.Services; +using GitHub.Models; +using Microsoft.VisualStudio.Text.Tagging; + +namespace GitHub.InlineReviews.Tags +{ + /// + /// Base class for inline comment tags. + /// + /// + /// + public abstract class InlineCommentTag : ITag + { + protected InlineCommentTag( + IPullRequestSession session, + int lineNumber, + DiffChangeType diffChangeType) + { + Guard.ArgumentNotNull(session, nameof(session)); + + LineNumber = lineNumber; + Session = session; + DiffChangeType = diffChangeType; + } + + public int LineNumber { get; } + public IPullRequestSession Session { get; } + public DiffChangeType DiffChangeType { get; } + } +} diff --git a/src/GitHub.InlineReviews/Tags/InlineCommentTagger.cs b/src/GitHub.InlineReviews/Tags/InlineCommentTagger.cs new file mode 100644 index 0000000000..3891003d63 --- /dev/null +++ b/src/GitHub.InlineReviews/Tags/InlineCommentTagger.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading.Tasks; +using GitHub.Extensions; +using GitHub.Logging; +using GitHub.Models; +using GitHub.Services; +using GitHub.InlineReviews.Margins; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Tagging; +using ReactiveUI; +using Serilog; + +namespace GitHub.InlineReviews.Tags +{ + /// + /// Creates tags in an for inline comment threads. + /// + public sealed class InlineCommentTagger : ITagger, IDisposable + { + static readonly ILogger log = LogManager.ForContext(); + static readonly IReadOnlyList> EmptyTags = new ITagSpan[0]; + readonly ITextBuffer buffer; + readonly ITextView view; + readonly IPullRequestSessionManager sessionManager; + bool needsInitialize = true; + string relativePath; + DiffSide side; + IPullRequestSession session; + IPullRequestSessionFile file; + IDisposable fileSubscription; + IDisposable sessionManagerSubscription; + IDisposable visibleSubscription; + + public InlineCommentTagger( + ITextView view, + ITextBuffer buffer, + IPullRequestSessionManager sessionManager) + { + Guard.ArgumentNotNull(buffer, nameof(buffer)); + Guard.ArgumentNotNull(sessionManager, nameof(sessionManager)); + + this.buffer = buffer; + this.view = view; + this.sessionManager = sessionManager; + } + + public bool ShowMargin => file?.Diff?.Count > 0; + + public event EventHandler TagsChanged; + + public void Dispose() + { + sessionManagerSubscription?.Dispose(); + sessionManagerSubscription = null; + fileSubscription?.Dispose(); + fileSubscription = null; + visibleSubscription?.Dispose(); + visibleSubscription = null; + } + + public IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans) + { + if (needsInitialize) + { + // Sucessful initialization will call NotifyTagsChanged, causing this method to be re-called. + ForgetWithLogging(Initialize()); + return EmptyTags; + } + else if (file?.InlineCommentThreads != null) + { + var result = new List>(); + var currentSession = session ?? sessionManager.CurrentSession; + + if (currentSession == null) + return EmptyTags; + + foreach (var span in spans) + { + var startLine = span.Start.GetContainingLine().LineNumber; + var endLine = span.End.GetContainingLine().LineNumber; + var linesWithComments = new BitArray((endLine - startLine) + 1); + var spanThreads = file.InlineCommentThreads.Where(x => + x.LineNumber >= startLine && + x.LineNumber <= endLine); + + foreach (var thread in spanThreads) + { + var snapshot = span.Snapshot; + var line = snapshot.GetLineFromLineNumber(thread.LineNumber); + + if ((side == DiffSide.Left && thread.DiffLineType == DiffChangeType.Delete) || + (side == DiffSide.Right && thread.DiffLineType != DiffChangeType.Delete)) + { + linesWithComments[thread.LineNumber - startLine] = true; + + result.Add(new TagSpan( + new SnapshotSpan(line.Start, line.End), + new ShowInlineCommentTag(currentSession, thread))); + } + } + + foreach (var chunk in file.Diff) + { + foreach (var line in chunk.Lines) + { + var lineNumber = (side == DiffSide.Left ? line.OldLineNumber : line.NewLineNumber) - 1; + + if (lineNumber >= startLine && + lineNumber <= endLine && + !linesWithComments[lineNumber - startLine] + && (side == DiffSide.Right || line.Type == DiffChangeType.Delete)) + { + var snapshotLine = span.Snapshot.GetLineFromLineNumber(lineNumber); + result.Add(new TagSpan( + new SnapshotSpan(snapshotLine.Start, snapshotLine.End), + new AddInlineCommentTag(currentSession, file.CommitSha, relativePath, line.DiffLineNumber, lineNumber, line.Type))); + } + } + } + } + + return result; + } + else + { + return EmptyTags; + } + } + + async Task Initialize() + { + needsInitialize = false; + + var bufferInfo = sessionManager.GetTextBufferInfo(buffer); + + if (bufferInfo != null) + { + var commitSha = bufferInfo.Side == DiffSide.Left ? "HEAD" : bufferInfo.CommitSha; + session = bufferInfo.Session; + relativePath = bufferInfo.RelativePath; + file = await session.GetFile(relativePath, commitSha); + fileSubscription = file.LinesChanged.Subscribe(LinesChanged); + side = bufferInfo.Side ?? DiffSide.Right; + NotifyTagsChanged(); + } + else + { + side = DiffSide.Right; + await InitializeLiveFile(); + sessionManagerSubscription = sessionManager + .WhenAnyValue(x => x.CurrentSession) + .Skip(1) + .Subscribe(_ => ForgetWithLogging(InitializeLiveFile())); + } + } + + async Task InitializeLiveFile() + { + fileSubscription?.Dispose(); + fileSubscription = null; + + relativePath = sessionManager.GetRelativePath(buffer); + + if (relativePath != null) + { + file = await sessionManager.GetLiveFile(relativePath, view, buffer); + + var options = view.Options; + visibleSubscription = + Observable.FromEventPattern(options, nameof(options.OptionChanged)) + .Select(_ => Unit.Default) + .StartWith(Unit.Default) + .Select(x => options.GetOptionValue(InlineCommentTextViewOptions.MarginVisibleId)) + .DistinctUntilChanged() + .Subscribe(VisibleChanged); + } + else + { + file = null; + } + + NotifyTagsChanged(); + } + + void VisibleChanged(bool enabled) + { + if (enabled) + { + fileSubscription = fileSubscription ?? file.LinesChanged.Subscribe(LinesChanged); + } + else + { + fileSubscription?.Dispose(); + fileSubscription = null; + } + } + + static void ForgetWithLogging(Task task) + { + task.Catch(e => log.Error(e, "Exception caught while executing background task")).Forget(); + } + + void LinesChanged(IReadOnlyList> lines) + { + NotifyTagsChanged(lines.Where(x => x.Item2 == side).Select(x => x.Item1)); + } + + void NotifyTagsChanged() + { + var entireFile = new SnapshotSpan(buffer.CurrentSnapshot, 0, buffer.CurrentSnapshot.Length); + TagsChanged?.Invoke(this, new SnapshotSpanEventArgs(entireFile)); + } + + void NotifyTagsChanged(int lineNumber) + { + var line = buffer.CurrentSnapshot.GetLineFromLineNumber(lineNumber); + var span = new SnapshotSpan(buffer.CurrentSnapshot, line.Start, line.Length); + TagsChanged?.Invoke(this, new SnapshotSpanEventArgs(span)); + } + + void NotifyTagsChanged(IEnumerable lineNumbers) + { + foreach (var lineNumber in lineNumbers) + { + NotifyTagsChanged(lineNumber); + } + } + } +} diff --git a/src/GitHub.InlineReviews/Tags/InlineCommentTaggerProvider.cs b/src/GitHub.InlineReviews/Tags/InlineCommentTaggerProvider.cs new file mode 100644 index 0000000000..fbdb02ab7d --- /dev/null +++ b/src/GitHub.InlineReviews/Tags/InlineCommentTaggerProvider.cs @@ -0,0 +1,41 @@ +using System; +using System.ComponentModel.Composition; +using GitHub.Extensions; +using GitHub.InlineReviews.Services; +using GitHub.Services; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Tagging; +using Microsoft.VisualStudio.Utilities; + +namespace GitHub.InlineReviews.Tags +{ + /// + /// Factory class for s. + /// + [Export(typeof(IViewTaggerProvider))] + [ContentType("text")] + [TagType(typeof(ShowInlineCommentTag))] + class InlineCommentTaggerProvider : IViewTaggerProvider + { + readonly IPullRequestSessionManager sessionManager; + + [ImportingConstructor] + public InlineCommentTaggerProvider( + IPullRequestSessionManager sessionManager) + { + Guard.ArgumentNotNull(sessionManager, nameof(sessionManager)); + + this.sessionManager = sessionManager; + } + + public ITagger CreateTagger(ITextView view, ITextBuffer buffer) where T : ITag + { + return buffer.Properties.GetOrCreateSingletonProperty(() => + new InlineCommentTagger( + view, + buffer, + sessionManager)) as ITagger; + } + } +} diff --git a/src/GitHub.InlineReviews/Tags/MouseEnterAndLeaveEventRouter.cs b/src/GitHub.InlineReviews/Tags/MouseEnterAndLeaveEventRouter.cs new file mode 100644 index 0000000000..103b399115 --- /dev/null +++ b/src/GitHub.InlineReviews/Tags/MouseEnterAndLeaveEventRouter.cs @@ -0,0 +1,105 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace GitHub.InlineReviews.Tags +{ + class MouseEnterAndLeaveEventRouter where T : FrameworkElement + { + T previousMouseOverElement; + + public void Add(UIElement sourceElement, UIElement targetElement) + { + sourceElement.MouseMove += (t, e) => MouseMove(targetElement, e); + sourceElement.MouseLeave += (t, e) => MouseLeave(targetElement, e); + } + + void MouseMove(object target, MouseEventArgs e) + { + T mouseOverElement = null; + Action visitAction = element => + { + mouseOverElement = element; + }; + + var visitor = new Visitor(e, visitAction); + visitor.Visit(target); + + if (mouseOverElement != previousMouseOverElement) + { + MouseLeave(previousMouseOverElement, e); + MouseEnter(mouseOverElement, e); + } + } + + void MouseLeave(object target, MouseEventArgs e) + { + MouseLeave(previousMouseOverElement, e); + } + + void MouseEnter(T element, MouseEventArgs e) + { + element?.RaiseEvent(new MouseEventArgs(e.MouseDevice, e.Timestamp) + { + RoutedEvent = Mouse.MouseEnterEvent, + }); + + previousMouseOverElement = element; + } + + void MouseLeave(T element, MouseEventArgs e) + { + element?.RaiseEvent(new MouseEventArgs(e.MouseDevice, e.Timestamp) + { + RoutedEvent = Mouse.MouseLeaveEvent, + }); + + previousMouseOverElement = null; + } + + class Visitor + { + MouseEventArgs mouseEventArgs; + Action action; + + internal Visitor(MouseEventArgs mouseEventArgs, Action action) + { + this.mouseEventArgs = mouseEventArgs; + this.action = action; + } + + internal void Visit(object obj) + { + if (obj is Panel) + { + Visit((Panel)obj); + return; + } + + if (obj is T) + { + Visit((T)obj); + return; + } + } + + internal void Visit(Panel panel) + { + foreach (var child in panel.Children) + { + Visit(child); + } + } + + internal void Visit(T element) + { + var point = mouseEventArgs.GetPosition(element); + if (point.Y >= 0 && point.Y < element.ActualHeight) + { + action(element); + } + } + } + } +} diff --git a/src/GitHub.InlineReviews/Tags/ShowInlineCommentGlyph.xaml b/src/GitHub.InlineReviews/Tags/ShowInlineCommentGlyph.xaml new file mode 100644 index 0000000000..77e7386777 --- /dev/null +++ b/src/GitHub.InlineReviews/Tags/ShowInlineCommentGlyph.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + diff --git a/src/GitHub.InlineReviews/Tags/ShowInlineCommentGlyph.xaml.cs b/src/GitHub.InlineReviews/Tags/ShowInlineCommentGlyph.xaml.cs new file mode 100644 index 0000000000..50dd329d61 --- /dev/null +++ b/src/GitHub.InlineReviews/Tags/ShowInlineCommentGlyph.xaml.cs @@ -0,0 +1,14 @@ +using System; +using System.Windows.Controls; + +namespace GitHub.InlineReviews.Tags +{ + public partial class ShowInlineCommentGlyph : UserControl + { + public ShowInlineCommentGlyph() + { + InitializeComponent(); + } + + } +} diff --git a/src/GitHub.InlineReviews/Tags/ShowInlineCommentTag.cs b/src/GitHub.InlineReviews/Tags/ShowInlineCommentTag.cs new file mode 100644 index 0000000000..b1071754cf --- /dev/null +++ b/src/GitHub.InlineReviews/Tags/ShowInlineCommentTag.cs @@ -0,0 +1,33 @@ +using System; +using GitHub.Extensions; +using GitHub.Models; +using GitHub.Services; + +namespace GitHub.InlineReviews.Tags +{ + /// + /// A tag which marks a line where inline review comments are present. + /// + public class ShowInlineCommentTag : InlineCommentTag + { + /// + /// Initializes a new instance of the class. + /// + /// The pull request session. + /// A model holding the details of the thread. + public ShowInlineCommentTag( + IPullRequestSession session, + IInlineCommentThreadModel thread) + : base(session, thread.LineNumber, thread.DiffLineType) + { + Guard.ArgumentNotNull(thread, nameof(thread)); + + Thread = thread; + } + + /// + /// Gets a model holding details of the thread at the tagged line. + /// + public IInlineCommentThreadModel Thread { get; } + } +} diff --git a/src/GitHub.InlineReviews/VSPackage.resx b/src/GitHub.InlineReviews/VSPackage.resx new file mode 100644 index 0000000000..b534be634e --- /dev/null +++ b/src/GitHub.InlineReviews/VSPackage.resx @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + GitHub.InlineReviews + + + A Visual Studio Extension that brings the GitHub Flow into Visual Studio. + + + + resources\logo_32x32@2x.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/src/GitHub.InlineReviews/ViewModels/CommentThreadViewModel.cs b/src/GitHub.InlineReviews/ViewModels/CommentThreadViewModel.cs new file mode 100644 index 0000000000..84f35c4695 --- /dev/null +++ b/src/GitHub.InlineReviews/ViewModels/CommentThreadViewModel.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.ObjectModel; +using System.Reactive; +using GitHub.Extensions; +using GitHub.Models; +using GitHub.ViewModels; +using ReactiveUI; + +namespace GitHub.InlineReviews.ViewModels +{ + /// + /// Base view model for a thread of comments. + /// + public abstract class CommentThreadViewModel : ReactiveObject, ICommentThreadViewModel + { + ReactiveCommand postComment; + ReactiveCommand editComment; + ReactiveCommand deleteComment; + + /// + /// Intializes a new instance of the class. + /// + /// The current user. + protected CommentThreadViewModel(ActorModel currentUser) + { + Guard.ArgumentNotNull(currentUser, nameof(currentUser)); + + Comments = new ObservableCollection(); + CurrentUser = new ActorViewModel(currentUser); + } + + /// + public ObservableCollection Comments { get; } + + /// + public ReactiveCommand PostComment + { + get { return postComment; } + protected set + { + Guard.ArgumentNotNull(value, nameof(value)); + postComment = value; + + // We want to ignore thrown exceptions from PostComment - the error should be handled + // by the CommentViewModel that trigged PostComment.Execute(); + value.ThrownExceptions.Subscribe(_ => { }); + } + } + + public ReactiveCommand EditComment + { + get { return editComment; } + protected set + { + Guard.ArgumentNotNull(value, nameof(value)); + editComment = value; + + value.ThrownExceptions.Subscribe(_ => { }); + } + } + + public ReactiveCommand DeleteComment + { + get { return deleteComment; } + protected set + { + Guard.ArgumentNotNull(value, nameof(value)); + deleteComment = value; + + value.ThrownExceptions.Subscribe(_ => { }); + } + } + + /// + public IActorViewModel CurrentUser { get; } + } +} diff --git a/src/GitHub.InlineReviews/ViewModels/CommentViewModel.cs b/src/GitHub.InlineReviews/ViewModels/CommentViewModel.cs new file mode 100644 index 0000000000..3565682991 --- /dev/null +++ b/src/GitHub.InlineReviews/ViewModels/CommentViewModel.cs @@ -0,0 +1,296 @@ +using System; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading.Tasks; +using System.Windows; +using GitHub.Extensions; +using GitHub.InlineReviews.Services; +using GitHub.Logging; +using GitHub.Models; +using GitHub.ViewModels; +using ReactiveUI; +using Serilog; + +namespace GitHub.InlineReviews.ViewModels +{ + /// + /// View model for an issue or pull request comment. + /// + public class CommentViewModel : ReactiveObject, ICommentViewModel + { + static readonly ILogger log = LogManager.ForContext(); + ICommentService commentService; + string body; + string errorMessage; + bool isReadOnly; + bool isSubmitting; + CommentEditState state; + DateTimeOffset updatedAt; + string undoBody; + ObservableAsPropertyHelper canDelete; + + /// + /// Initializes a new instance of the class. + /// + /// The comment service + /// The thread that the comment is a part of. + /// The current user. + /// The pull request id of the comment. + /// The GraphQL ID of the comment. + /// The database id of the comment. + /// The comment body. + /// The comment edit state. + /// The author of the comment. + /// The modified date of the comment. + /// + protected CommentViewModel( + ICommentService commentService, + ICommentThreadViewModel thread, + IActorViewModel currentUser, + int pullRequestId, + string commentId, + int databaseId, + string body, + CommentEditState state, + IActorViewModel author, + DateTimeOffset updatedAt, + Uri webUrl) + { + this.commentService = commentService; + Guard.ArgumentNotNull(thread, nameof(thread)); + Guard.ArgumentNotNull(currentUser, nameof(currentUser)); + Guard.ArgumentNotNull(author, nameof(author)); + + Thread = thread; + CurrentUser = currentUser; + Id = commentId; + DatabaseId = databaseId; + PullRequestId = pullRequestId; + Body = body; + EditState = state; + Author = author; + UpdatedAt = updatedAt; + WebUrl = webUrl; + + var canDeleteObservable = this.WhenAnyValue( + x => x.EditState, + x => x == CommentEditState.None && author.Login == currentUser.Login); + + canDelete = canDeleteObservable.ToProperty(this, x => x.CanDelete); + + Delete = ReactiveCommand.CreateAsyncTask(canDeleteObservable, DoDelete); + + var canEdit = this.WhenAnyValue( + x => x.EditState, + x => x == CommentEditState.Placeholder || (x == CommentEditState.None && author.Login == currentUser.Login)); + + BeginEdit = ReactiveCommand.Create(canEdit); + BeginEdit.Subscribe(DoBeginEdit); + AddErrorHandler(BeginEdit); + + CommitEdit = ReactiveCommand.CreateAsyncTask( + Observable.CombineLatest( + this.WhenAnyValue(x => x.IsReadOnly), + this.WhenAnyValue(x => x.Body, x => !string.IsNullOrWhiteSpace(x)), + this.WhenAnyObservable(x => x.Thread.PostComment.CanExecuteObservable), + (readOnly, hasBody, canPost) => !readOnly && hasBody && canPost), + DoCommitEdit); + AddErrorHandler(CommitEdit); + + CancelEdit = ReactiveCommand.Create(CommitEdit.IsExecuting.Select(x => !x)); + CancelEdit.Subscribe(DoCancelEdit); + AddErrorHandler(CancelEdit); + + OpenOnGitHub = ReactiveCommand.Create(this.WhenAnyValue(x => x.Id).Select(x => x != null)); + } + + /// + /// Initializes a new instance of the class. + /// + /// Comment Service + /// The thread that the comment is a part of. + /// The current user. + /// The comment model. + protected CommentViewModel( + ICommentService commentService, + ICommentThreadViewModel thread, + ActorModel currentUser, + CommentModel model) + : this( + commentService, + thread, + new ActorViewModel(currentUser), + model.PullRequestId, + model.Id, + model.DatabaseId, + model.Body, + CommentEditState.None, + new ActorViewModel(model.Author), + model.CreatedAt, + new Uri(model.Url)) + { + } + + protected void AddErrorHandler(ReactiveCommand command) + { + command.ThrownExceptions.Subscribe(x => ErrorMessage = x.Message); + } + + async Task DoDelete(object unused) + { + if (commentService.ConfirmCommentDelete()) + { + try + { + ErrorMessage = null; + IsSubmitting = true; + + await Thread.DeleteComment.ExecuteAsyncTask(new Tuple(PullRequestId, DatabaseId)); + } + catch (Exception e) + { + var message = e.Message; + ErrorMessage = message; + log.Error(e, "Error Deleting comment"); + } + finally + { + IsSubmitting = false; + } + } + } + + void DoBeginEdit(object unused) + { + if (state != CommentEditState.Editing) + { + ErrorMessage = null; + undoBody = Body; + EditState = CommentEditState.Editing; + } + } + + void DoCancelEdit(object unused) + { + if (EditState == CommentEditState.Editing) + { + EditState = string.IsNullOrWhiteSpace(undoBody) ? CommentEditState.Placeholder : CommentEditState.None; + Body = undoBody; + ErrorMessage = null; + undoBody = null; + } + } + + async Task DoCommitEdit(object unused) + { + try + { + ErrorMessage = null; + IsSubmitting = true; + + if (Id == null) + { + await Thread.PostComment.ExecuteAsyncTask(Body); + } + else + { + await Thread.EditComment.ExecuteAsyncTask(new Tuple(Id, Body)); + } + } + catch (Exception e) + { + var message = e.Message; + ErrorMessage = message; + log.Error(e, "Error posting comment"); + } + finally + { + IsSubmitting = false; + } + } + + /// + public string Id { get; private set; } + + /// + public int DatabaseId { get; private set; } + + /// + public int PullRequestId { get; private set; } + + /// + public string Body + { + get { return body; } + set { this.RaiseAndSetIfChanged(ref body, value); } + } + + /// + public string ErrorMessage + { + get { return this.errorMessage; } + private set { this.RaiseAndSetIfChanged(ref errorMessage, value); } + } + + /// + public CommentEditState EditState + { + get { return state; } + private set { this.RaiseAndSetIfChanged(ref state, value); } + } + + /// + public bool IsReadOnly + { + get { return isReadOnly; } + set { this.RaiseAndSetIfChanged(ref isReadOnly, value); } + } + + /// + public bool IsSubmitting + { + get { return isSubmitting; } + protected set { this.RaiseAndSetIfChanged(ref isSubmitting, value); } + } + + public bool CanDelete + { + get { return canDelete.Value; } + } + + /// + public DateTimeOffset UpdatedAt + { + get { return updatedAt; } + private set { this.RaiseAndSetIfChanged(ref updatedAt, value); } + } + + /// + public IActorViewModel CurrentUser { get; } + + /// + public ICommentThreadViewModel Thread { get; } + + /// + public IActorViewModel Author { get; } + + /// + public Uri WebUrl { get; } + + /// + public ReactiveCommand BeginEdit { get; } + + /// + public ReactiveCommand CancelEdit { get; } + + /// + public ReactiveCommand CommitEdit { get; } + + /// + public ReactiveCommand OpenOnGitHub { get; } + + /// + public ReactiveCommand Delete { get; } + } +} diff --git a/src/GitHub.InlineReviews/ViewModels/ICommentThreadViewModel.cs b/src/GitHub.InlineReviews/ViewModels/ICommentThreadViewModel.cs new file mode 100644 index 0000000000..b17290de59 --- /dev/null +++ b/src/GitHub.InlineReviews/ViewModels/ICommentThreadViewModel.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.ObjectModel; +using System.Reactive; +using GitHub.Models; +using GitHub.ViewModels; +using ReactiveUI; + +namespace GitHub.InlineReviews.ViewModels +{ + /// + /// A comment thread. + /// + public interface ICommentThreadViewModel + { + /// + /// Gets the comments in the thread. + /// + ObservableCollection Comments { get; } + + /// + /// Gets the current user under whos account new comments will be created. + /// + IActorViewModel CurrentUser { get; } + + /// + /// Called by a comment in the thread to post itself as a new comment to the API. + /// + ReactiveCommand PostComment { get; } + + /// + /// Called by a comment in the thread to post itself as an edit to a comment to the API. + /// + ReactiveCommand EditComment { get; } + + /// + /// Called by a comment in the thread to send a delete of the comment to the API. + /// + ReactiveCommand DeleteComment { get; } + } +} diff --git a/src/GitHub.InlineReviews/ViewModels/ICommentViewModel.cs b/src/GitHub.InlineReviews/ViewModels/ICommentViewModel.cs new file mode 100644 index 0000000000..9bb64b0bfb --- /dev/null +++ b/src/GitHub.InlineReviews/ViewModels/ICommentViewModel.cs @@ -0,0 +1,112 @@ +using System; +using System.Reactive; +using GitHub.Models; +using GitHub.ViewModels; +using ReactiveUI; + +namespace GitHub.InlineReviews.ViewModels +{ + public enum CommentEditState + { + None, + Editing, + Placeholder, + } + + /// + /// View model for an issue or pull request comment. + /// + public interface ICommentViewModel : IViewModel + { + /// + /// Gets the GraphQL ID of the comment. + /// + string Id { get; } + + /// + /// Gets the Database ID of the comment. + /// + int DatabaseId { get; } + + /// + /// The pull request id of the comment + /// + int PullRequestId { get; } + + /// + /// Gets or sets the body of the comment. + /// + string Body { get; set; } + + /// + /// Gets any error message encountered posting or updating the comment. + /// + string ErrorMessage { get; } + + /// + /// Gets the current edit state of the comment. + /// + CommentEditState EditState { get; } + + /// + /// Gets or sets a value indicating whether the comment is read-only. + /// + bool IsReadOnly { get; set; } + + /// + /// Gets a value indicating whether the comment is currently in the process of being + /// submitted. + /// + bool IsSubmitting { get; } + + /// + /// Gets a value indicating whether the comment can be edited or deleted by the current user + /// + bool CanDelete { get; } + + /// + /// Gets the modified date of the comment. + /// + DateTimeOffset UpdatedAt { get; } + + /// + /// Gets the author of the comment. + /// + IActorViewModel Author { get; } + + /// + /// Gets the thread that the comment is a part of. + /// + ICommentThreadViewModel Thread { get; } + + /// + /// Gets the URL of the comment on the web. + /// + Uri WebUrl { get; } + + /// + /// Gets a command which will begin editing of the comment. + /// + ReactiveCommand BeginEdit { get; } + + /// + /// Gets a command which will cancel editing of the comment. + /// + ReactiveCommand CancelEdit { get; } + + /// + /// Gets a command which will commit edits to the comment. + /// + ReactiveCommand CommitEdit { get; } + + /// + /// Gets a command to open the comment in a browser. + /// + ReactiveCommand OpenOnGitHub { get; } + + /// + /// Deletes a comment. + /// + ReactiveCommand Delete { get; } + } +} \ No newline at end of file diff --git a/src/GitHub.InlineReviews/ViewModels/IPullRequestReviewCommentViewModel.cs b/src/GitHub.InlineReviews/ViewModels/IPullRequestReviewCommentViewModel.cs new file mode 100644 index 0000000000..63bb86d96d --- /dev/null +++ b/src/GitHub.InlineReviews/ViewModels/IPullRequestReviewCommentViewModel.cs @@ -0,0 +1,36 @@ +using System; +using System.Reactive; +using ReactiveUI; + +namespace GitHub.InlineReviews.ViewModels +{ + /// + /// View model for a pull request review comment. + /// + public interface IPullRequestReviewCommentViewModel : ICommentViewModel + { + /// + /// Gets a value indicating whether the user can start a new review with this comment. + /// + bool CanStartReview { get; } + + /// + /// Gets the caption for the "Commit" button. + /// + /// + /// This will be "Add a single comment" when not in review mode and "Add review comment" + /// when in review mode. + /// + string CommitCaption { get; } + + /// + /// Gets a value indicating whether this comment is part of a pending pull request review. + /// + bool IsPending { get; } + + /// + /// Gets a command which will commit a new comment and start a review. + /// + ReactiveCommand StartReview { get; } + } +} \ No newline at end of file diff --git a/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs b/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs new file mode 100644 index 0000000000..91bef39588 --- /dev/null +++ b/src/GitHub.InlineReviews/ViewModels/InlineCommentPeekViewModel.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading.Tasks; +using GitHub.Api; +using GitHub.Commands; +using GitHub.Extensions; +using GitHub.Extensions.Reactive; +using GitHub.Factories; +using GitHub.InlineReviews.Commands; +using GitHub.InlineReviews.Peek; +using GitHub.InlineReviews.Services; +using GitHub.Logging; +using GitHub.Models; +using GitHub.Primitives; +using GitHub.Services; +using Microsoft.VisualStudio.Language.Intellisense; +using Microsoft.VisualStudio.Text; +using ReactiveUI; +using Serilog; + +namespace GitHub.InlineReviews.ViewModels +{ + /// + /// Represents the contents of an inline comment peek view displayed in an editor. + /// + public sealed class InlineCommentPeekViewModel : ReactiveObject, IDisposable + { + static readonly ILogger log = LogManager.ForContext(); + readonly IInlineCommentPeekService peekService; + readonly IPeekSession peekSession; + readonly IPullRequestSessionManager sessionManager; + readonly ICommentService commentService; + IPullRequestSession session; + IPullRequestSessionFile file; + ICommentThreadViewModel thread; + IDisposable fileSubscription; + IDisposable sessionSubscription; + IDisposable threadSubscription; + ITrackingPoint triggerPoint; + string relativePath; + DiffSide side; + + /// + /// Initializes a new instance of the class. + /// + public InlineCommentPeekViewModel(IInlineCommentPeekService peekService, + IPeekSession peekSession, + IPullRequestSessionManager sessionManager, + INextInlineCommentCommand nextCommentCommand, + IPreviousInlineCommentCommand previousCommentCommand, + ICommentService commentService) + { + Guard.ArgumentNotNull(peekService, nameof(peekService)); + Guard.ArgumentNotNull(peekSession, nameof(peekSession)); + Guard.ArgumentNotNull(sessionManager, nameof(sessionManager)); + Guard.ArgumentNotNull(nextCommentCommand, nameof(nextCommentCommand)); + Guard.ArgumentNotNull(previousCommentCommand, nameof(previousCommentCommand)); + + this.peekService = peekService; + this.peekSession = peekSession; + this.sessionManager = sessionManager; + this.commentService = commentService; + triggerPoint = peekSession.GetTriggerPoint(peekSession.TextView.TextBuffer); + + peekSession.Dismissed += (s, e) => Dispose(); + + Close = this.WhenAnyValue(x => x.Thread) + .SelectMany(x => x is NewInlineCommentThreadViewModel + ? x.Comments.Single().CancelEdit.SelectUnit() + : Observable.Never()); + + NextComment = ReactiveCommand.CreateAsyncTask( + Observable.Return(nextCommentCommand.Enabled), + _ => nextCommentCommand.Execute(new InlineCommentNavigationParams + { + FromLine = peekService.GetLineNumber(peekSession, triggerPoint).Item1, + })); + + PreviousComment = ReactiveCommand.CreateAsyncTask( + Observable.Return(previousCommentCommand.Enabled), + _ => previousCommentCommand.Execute(new InlineCommentNavigationParams + { + FromLine = peekService.GetLineNumber(peekSession, triggerPoint).Item1, + })); + } + + /// + /// Gets the thread of comments to display. + /// + public ICommentThreadViewModel Thread + { + get { return thread; } + private set { this.RaiseAndSetIfChanged(ref thread, value); } + } + + /// + /// Gets a command which moves to the next inline comment in the file. + /// + public ReactiveCommand NextComment { get; } + + /// + /// Gets a command which moves to the previous inline comment in the file. + /// + public ReactiveCommand PreviousComment { get; } + + public IObservable Close { get; } + + public void Dispose() + { + threadSubscription?.Dispose(); + threadSubscription = null; + sessionSubscription?.Dispose(); + sessionSubscription = null; + fileSubscription?.Dispose(); + fileSubscription = null; + } + + public async Task Initialize() + { + var buffer = peekSession.TextView.TextBuffer; + var info = sessionManager.GetTextBufferInfo(buffer); + + if (info != null) + { + var commitSha = info.Side == DiffSide.Left ? "HEAD" : info.CommitSha; + relativePath = info.RelativePath; + side = info.Side ?? DiffSide.Right; + file = await info.Session.GetFile(relativePath, commitSha); + session = info.Session; + await UpdateThread(); + } + else + { + relativePath = sessionManager.GetRelativePath(buffer); + side = DiffSide.Right; + file = await sessionManager.GetLiveFile(relativePath, peekSession.TextView, buffer); + await SessionChanged(sessionManager.CurrentSession); + sessionSubscription = sessionManager.WhenAnyValue(x => x.CurrentSession) + .Skip(1) + .Subscribe(x => SessionChanged(x).Forget()); + } + + fileSubscription?.Dispose(); + fileSubscription = file.LinesChanged.Subscribe(LinesChanged); + } + + async void LinesChanged(IReadOnlyList> lines) + { + try + { + var lineNumber = peekService.GetLineNumber(peekSession, triggerPoint).Item1; + + if (lines.Contains(Tuple.Create(lineNumber, side))) + { + await UpdateThread(); + } + } + catch (Exception e) + { + log.Error(e, "Error updating InlineCommentViewModel"); + } + } + + async Task UpdateThread() + { + var placeholderBody = GetPlaceholderBodyToPreserve(); + + Thread = null; + threadSubscription?.Dispose(); + + if (file == null) + return; + + var lineAndLeftBuffer = peekService.GetLineNumber(peekSession, triggerPoint); + var lineNumber = lineAndLeftBuffer.Item1; + var leftBuffer = lineAndLeftBuffer.Item2; + var thread = file.InlineCommentThreads?.FirstOrDefault(x => + x.LineNumber == lineNumber && + ((leftBuffer && x.DiffLineType == DiffChangeType.Delete) || (!leftBuffer && x.DiffLineType != DiffChangeType.Delete))); + + if (thread != null) + { + Thread = new InlineCommentThreadViewModel(commentService, session, thread.Comments); + } + else + { + Thread = new NewInlineCommentThreadViewModel(commentService, session, file, lineNumber, leftBuffer); + } + + if (!string.IsNullOrWhiteSpace(placeholderBody)) + { + var placeholder = Thread.Comments.LastOrDefault(); + + if (placeholder?.EditState == CommentEditState.Placeholder) + { + await placeholder.BeginEdit.ExecuteAsync(null); + placeholder.Body = placeholderBody; + } + } + } + + async Task SessionChanged(IPullRequestSession pullRequestSession) + { + this.session = pullRequestSession; + + if (pullRequestSession == null) + { + Thread = null; + threadSubscription?.Dispose(); + threadSubscription = null; + return; + } + else + { + await UpdateThread(); + } + } + + string GetPlaceholderBodyToPreserve() + { + var lastComment = Thread?.Comments.LastOrDefault(); + + if (lastComment?.EditState == CommentEditState.Editing) + { + if (!lastComment.IsSubmitting) return lastComment.Body; + } + + return null; + } + } +} diff --git a/src/GitHub.InlineReviews/ViewModels/InlineCommentThreadViewModel.cs b/src/GitHub.InlineReviews/ViewModels/InlineCommentThreadViewModel.cs new file mode 100644 index 0000000000..8c2d1567fd --- /dev/null +++ b/src/GitHub.InlineReviews/ViewModels/InlineCommentThreadViewModel.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reactive.Linq; +using System.Threading.Tasks; +using GitHub.Extensions; +using GitHub.InlineReviews.Services; +using GitHub.Models; +using GitHub.Services; +using ReactiveUI; + +namespace GitHub.InlineReviews.ViewModels +{ + /// + /// A thread of inline comments (aka Pull Request Review Comments). + /// + public class InlineCommentThreadViewModel : CommentThreadViewModel + { + /// + /// Initializes a new instance of the class. + /// + /// The comment service + /// The current PR review session. + /// The comments to display in this inline review. + public InlineCommentThreadViewModel(ICommentService commentService, IPullRequestSession session, + IEnumerable comments) + : base(session.User) + { + Guard.ArgumentNotNull(session, nameof(session)); + + Session = session; + + PostComment = ReactiveCommand.CreateAsyncTask( + Observable.Return(true), + DoPostComment); + + EditComment = ReactiveCommand.CreateAsyncTask( + Observable.Return(true), + DoEditComment); + + DeleteComment = ReactiveCommand.CreateAsyncTask( + Observable.Return(true), + DoDeleteComment); + + foreach (var comment in comments) + { + Comments.Add(new PullRequestReviewCommentViewModel( + session, + commentService, + this, + CurrentUser, + comment.Review, + comment.Comment)); + } + + Comments.Add(PullRequestReviewCommentViewModel.CreatePlaceholder(session, commentService, this, CurrentUser)); + } + + /// + /// Gets the current pull request review session. + /// + public IPullRequestSession Session { get; } + + async Task DoPostComment(object parameter) + { + Guard.ArgumentNotNull(parameter, nameof(parameter)); + + var body = (string)parameter; + var replyId = Comments[0].Id; + await Session.PostReviewComment(body, replyId); + } + + async Task DoEditComment(object parameter) + { + Guard.ArgumentNotNull(parameter, nameof(parameter)); + + var item = (Tuple)parameter; + await Session.EditComment(item.Item1, item.Item2); + } + + async Task DoDeleteComment(object parameter) + { + Guard.ArgumentNotNull(parameter, nameof(parameter)); + + var item = (Tuple)parameter; + await Session.DeleteComment(item.Item1, item.Item2); + } + } +} diff --git a/src/GitHub.InlineReviews/ViewModels/NewInlineCommentThreadViewModel.cs b/src/GitHub.InlineReviews/ViewModels/NewInlineCommentThreadViewModel.cs new file mode 100644 index 0000000000..a9753a4816 --- /dev/null +++ b/src/GitHub.InlineReviews/ViewModels/NewInlineCommentThreadViewModel.cs @@ -0,0 +1,122 @@ +using System; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading.Tasks; +using GitHub.Extensions; +using GitHub.InlineReviews.Services; +using GitHub.Models; +using GitHub.Services; +using ReactiveUI; + +namespace GitHub.InlineReviews.ViewModels +{ + /// + /// A new inline comment thread that is being authored. + /// + public class NewInlineCommentThreadViewModel : CommentThreadViewModel + { + bool needsPush; + + /// + /// Initializes a new instance of the class. + /// + /// The comment service + /// The current PR review session. + /// The file being commented on. + /// The 0-based line number in the file. + /// + /// True if the comment is being left on the left-hand-side of a diff; otherwise false. + /// + public NewInlineCommentThreadViewModel(ICommentService commentService, + IPullRequestSession session, + IPullRequestSessionFile file, + int lineNumber, + bool leftComparisonBuffer) + : base(session.User) + { + Guard.ArgumentNotNull(session, nameof(session)); + Guard.ArgumentNotNull(file, nameof(file)); + + Session = session; + File = file; + LineNumber = lineNumber; + LeftComparisonBuffer = leftComparisonBuffer; + + PostComment = ReactiveCommand.CreateAsyncTask( + this.WhenAnyValue(x => x.NeedsPush, x => !x), + DoPostComment); + + EditComment = ReactiveCommand.CreateAsyncTask( + Observable.Return(false), + o => null); + + DeleteComment = ReactiveCommand.CreateAsyncTask( + Observable.Return(false), + o => null); + + var placeholder = PullRequestReviewCommentViewModel.CreatePlaceholder(session, commentService, this, CurrentUser); + placeholder.BeginEdit.Execute(null); + this.WhenAnyValue(x => x.NeedsPush).Subscribe(x => placeholder.IsReadOnly = x); + Comments.Add(placeholder); + + file.WhenAnyValue(x => x.CommitSha).Subscribe(x => NeedsPush = x == null); + } + + /// + /// Gets the file that the comment will be left on. + /// + public IPullRequestSessionFile File { get; } + + /// + /// Gets the 0-based line number in the file that the comment will be left on. + /// + public int LineNumber { get; } + + /// + /// Gets a value indicating whether comment is being left on the left-hand-side of a diff. + /// + public bool LeftComparisonBuffer { get; } + + /// + /// Gets the current pull request review session. + /// + public IPullRequestSession Session { get; } + + /// + /// Gets a value indicating whether the user must commit and push their changes before + /// leaving a comment on the requested line. + /// + public bool NeedsPush + { + get { return needsPush; } + private set { this.RaiseAndSetIfChanged(ref needsPush, value); } + } + + async Task DoPostComment(object parameter) + { + Guard.ArgumentNotNull(parameter, nameof(parameter)); + + var diffPosition = File.Diff + .SelectMany(x => x.Lines) + .FirstOrDefault(x => + { + var line = LeftComparisonBuffer ? x.OldLineNumber : x.NewLineNumber; + return line == LineNumber + 1; + }); + + if (diffPosition == null) + { + throw new InvalidOperationException("Unable to locate line in diff."); + } + + var body = (string)parameter; + await Session.PostReviewComment( + body, + File.CommitSha, + File.RelativePath.Replace("\\", "/"), + File.Diff, + diffPosition.DiffLineNumber); + } + } +} diff --git a/src/GitHub.InlineReviews/ViewModels/PullRequestFileMarginViewModel.cs b/src/GitHub.InlineReviews/ViewModels/PullRequestFileMarginViewModel.cs new file mode 100644 index 0000000000..84968d9d85 --- /dev/null +++ b/src/GitHub.InlineReviews/ViewModels/PullRequestFileMarginViewModel.cs @@ -0,0 +1,53 @@ +using System; +using System.Windows.Input; +using GitHub.Commands; +using GitHub.Services; +using ReactiveUI; + +namespace GitHub.InlineReviews.ViewModels +{ + public class PullRequestFileMarginViewModel : ReactiveObject + { + bool enabled; + string fileName; + int commentsInFile; + bool marginEnabled; + + public PullRequestFileMarginViewModel(ICommand toggleInlineCommentMarginCommand, ICommand viewChangesCommand, + Lazy usageTracker) + { + ToggleInlineCommentMarginCommand = toggleInlineCommentMarginCommand = new UsageTrackingCommand( + usageTracker, x => x.NumberOfPullRequestFileMarginToggleInlineCommentMargin, toggleInlineCommentMarginCommand); + ViewChangesCommand = viewChangesCommand = new UsageTrackingCommand( + usageTracker, x => x.NumberOfPullRequestFileMarginViewChanges, viewChangesCommand); + } + + public bool Enabled + { + get { return enabled; } + set { this.RaiseAndSetIfChanged(ref enabled, value); } + } + + public string FileName + { + get { return fileName; } + set { this.RaiseAndSetIfChanged(ref fileName, value); } + } + + public int CommentsInFile + { + get { return commentsInFile; } + set { this.RaiseAndSetIfChanged(ref commentsInFile, value); } + } + + public bool MarginEnabled + { + get { return marginEnabled; } + set { this.RaiseAndSetIfChanged(ref marginEnabled, value); } + } + + public ICommand ToggleInlineCommentMarginCommand { get; } + + public ICommand ViewChangesCommand { get; } + } +} diff --git a/src/GitHub.InlineReviews/ViewModels/PullRequestReviewCommentViewModel.cs b/src/GitHub.InlineReviews/ViewModels/PullRequestReviewCommentViewModel.cs new file mode 100644 index 0000000000..047000f734 --- /dev/null +++ b/src/GitHub.InlineReviews/ViewModels/PullRequestReviewCommentViewModel.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading.Tasks; +using GitHub.Extensions; +using GitHub.InlineReviews.Services; +using GitHub.Logging; +using GitHub.Models; +using GitHub.Services; +using GitHub.ViewModels; +using GitHub.VisualStudio.UI; +using ReactiveUI; +using Serilog; + +namespace GitHub.InlineReviews.ViewModels +{ + /// + /// View model for a pull request review comment. + /// + public class PullRequestReviewCommentViewModel : CommentViewModel, IPullRequestReviewCommentViewModel + { + readonly IPullRequestSession session; + ObservableAsPropertyHelper canStartReview; + ObservableAsPropertyHelper commitCaption; + + /// + /// Initializes a new instance of the class. + /// + /// The pull request session. + /// The comment service + /// The thread that the comment is a part of. + /// The current user. + /// The pull request id of the comment. + /// The GraphQL ID of the comment. + /// The database id of the comment. + /// The comment body. + /// The comment edit state. + /// The author of the comment. + /// The modified date of the comment. + /// Whether this is a pending comment. + /// + public PullRequestReviewCommentViewModel( + IPullRequestSession session, + ICommentService commentService, + ICommentThreadViewModel thread, + IActorViewModel currentUser, + int pullRequestId, + string commentId, + int databaseId, + string body, + CommentEditState state, + IActorViewModel author, + DateTimeOffset updatedAt, + bool isPending, + Uri webUrl) + : base(commentService, thread, currentUser, pullRequestId, commentId, databaseId, body, state, author, updatedAt, webUrl) + { + Guard.ArgumentNotNull(session, nameof(session)); + + this.session = session; + IsPending = isPending; + + var pendingReviewAndIdObservable = Observable.CombineLatest( + session.WhenAnyValue(x => x.HasPendingReview, x => !x), + this.WhenAnyValue(model => model.Id).Select(i => i == null), + (hasPendingReview, isNewComment) => new { hasPendingReview, isNewComment }); + + canStartReview = pendingReviewAndIdObservable + .Select(arg => arg.hasPendingReview && arg.isNewComment) + .ToProperty(this, x => x.CanStartReview); + + commitCaption = pendingReviewAndIdObservable + .Select(arg => !arg.isNewComment ? Resources.UpdateComment : arg.hasPendingReview ? Resources.AddSingleComment : Resources.AddReviewComment) + .ToProperty(this, x => x.CommitCaption); + + StartReview = ReactiveCommand.CreateAsyncTask( + CommitEdit.CanExecuteObservable, + DoStartReview); + AddErrorHandler(StartReview); + } + + /// + /// Initializes a new instance of the class. + /// + /// The pull request session. + /// Comment Service + /// The thread that the comment is a part of. + /// The current user. + /// The associated pull request review. + /// The comment model. + public PullRequestReviewCommentViewModel( + IPullRequestSession session, + ICommentService commentService, + ICommentThreadViewModel thread, + IActorViewModel currentUser, + PullRequestReviewModel review, + PullRequestReviewCommentModel model) + : this( + session, + commentService, + thread, + currentUser, + model.PullRequestId, + model.Id, + model.DatabaseId, + model.Body, + CommentEditState.None, + new ActorViewModel(model.Author), + model.CreatedAt, + review.State == PullRequestReviewState.Pending, + model.Url != null ? new Uri(model.Url) : null) + { + } + + /// + /// Creates a placeholder comment which can be used to add a new comment to a thread. + /// + /// The pull request session. + /// Comment Service + /// The comment thread. + /// The current user. + /// THe placeholder comment. + public static CommentViewModel CreatePlaceholder( + IPullRequestSession session, + ICommentService commentService, + ICommentThreadViewModel thread, + IActorViewModel currentUser) + { + return new PullRequestReviewCommentViewModel( + session, + commentService, + thread, + currentUser, + 0, + null, + 0, + string.Empty, + CommentEditState.Placeholder, + currentUser, + DateTimeOffset.MinValue, + false, + null); + } + + /// + public bool CanStartReview => canStartReview.Value; + + /// + public string CommitCaption => commitCaption.Value; + + /// + public bool IsPending { get; } + + /// + public ReactiveCommand StartReview { get; } + + async Task DoStartReview(object unused) + { + IsSubmitting = true; + + try + { + await session.StartReview(); + await CommitEdit.ExecuteAsync(null); + } + finally + { + IsSubmitting = false; + } + } + } +} diff --git a/src/GitHub.InlineReviews/ViewModels/PullRequestStatusViewModel.cs b/src/GitHub.InlineReviews/ViewModels/PullRequestStatusViewModel.cs new file mode 100644 index 0000000000..9c59ad2379 --- /dev/null +++ b/src/GitHub.InlineReviews/ViewModels/PullRequestStatusViewModel.cs @@ -0,0 +1,49 @@ +using System; +using System.Windows.Input; +using System.ComponentModel; + +namespace GitHub.InlineReviews.ViewModels +{ + public class PullRequestStatusViewModel : INotifyPropertyChanged + { + int? number; + string title; + + public PullRequestStatusViewModel(ICommand openPullRequestsCommand, ICommand showCurrentPullRequestCommand) + { + OpenPullRequestsCommand = openPullRequestsCommand; + 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 OpenPullRequestsCommand { get; } + public ICommand ShowCurrentPullRequestCommand { get; } + + public event PropertyChangedEventHandler PropertyChanged; + } +} diff --git a/src/GitHub.InlineReviews/Views/CommentThreadView.xaml b/src/GitHub.InlineReviews/Views/CommentThreadView.xaml new file mode 100644 index 0000000000..6d8c904166 --- /dev/null +++ b/src/GitHub.InlineReviews/Views/CommentThreadView.xaml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + diff --git a/src/GitHub.InlineReviews/Views/CommentThreadView.xaml.cs b/src/GitHub.InlineReviews/Views/CommentThreadView.xaml.cs new file mode 100644 index 0000000000..3143ef3e42 --- /dev/null +++ b/src/GitHub.InlineReviews/Views/CommentThreadView.xaml.cs @@ -0,0 +1,15 @@ +using System; +using System.Windows.Controls; +using GitHub.VisualStudio.UI.Helpers; + +namespace GitHub.InlineReviews.Views +{ + public partial class CommentThreadView : UserControl + { + public CommentThreadView() + { + InitializeComponent(); + PreviewMouseWheel += ScrollViewerUtilities.FixMouseWheelScroll; + } + } +} diff --git a/src/GitHub.InlineReviews/Views/CommentView.xaml b/src/GitHub.InlineReviews/Views/CommentView.xaml new file mode 100644 index 0000000000..7e49028622 --- /dev/null +++ b/src/GitHub.InlineReviews/Views/CommentView.xaml @@ -0,0 +1,226 @@ + + + + + You can use a `CompositeDisposable` type here, it's designed to handle disposables in an optimal way (you can just call `Dispose()` on it and it will handle disposing everything it holds). + + + + + + + + + + + + + + + + + + + + + + + + Pending + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GitHub.InlineReviews/Views/CommentView.xaml.cs b/src/GitHub.InlineReviews/Views/CommentView.xaml.cs new file mode 100644 index 0000000000..271fac0502 --- /dev/null +++ b/src/GitHub.InlineReviews/Views/CommentView.xaml.cs @@ -0,0 +1,74 @@ +using System; +using System.Windows.Input; +using GitHub.InlineReviews.ViewModels; +using GitHub.Services; +using GitHub.UI; +using Microsoft.VisualStudio.Shell; +using ReactiveUI; + +namespace GitHub.InlineReviews.Views +{ + public class GenericCommentView : ViewBase { } + + public partial class CommentView : GenericCommentView + { + public CommentView() + { + InitializeComponent(); + this.Loaded += CommentView_Loaded; + + this.WhenActivated(d => + { + d(ViewModel.OpenOnGitHub.Subscribe(_ => DoOpenOnGitHub())); + }); + } + + IVisualStudioBrowser GetBrowser() + { + var serviceProvider = (IGitHubServiceProvider)Package.GetGlobalService(typeof(IGitHubServiceProvider)); + return serviceProvider.GetService(); + } + + void DoOpenOnGitHub() + { + GetBrowser().OpenUrl(ViewModel.WebUrl); + } + + private void CommentView_Loaded(object sender, System.Windows.RoutedEventArgs e) + { + if (buttonPanel.IsVisible) + { + BringIntoView(); + body.Focus(); + } + } + + private void ReplyPlaceholder_GotFocus(object sender, System.Windows.RoutedEventArgs e) + { + var command = ((ICommentViewModel)DataContext)?.BeginEdit; + + if (command?.CanExecute(null) == true) + { + command.Execute(null); + } + } + + private void buttonPanel_IsVisibleChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e) + { + if (buttonPanel.IsVisible) + { + BringIntoView(); + } + } + + void OpenHyperlink(object sender, ExecutedRoutedEventArgs e) + { + Uri uri; + + if (Uri.TryCreate(e.Parameter?.ToString(), UriKind.Absolute, out uri)) + { + GetBrowser().OpenUrl(uri); + } + } + } +} diff --git a/src/GitHub.InlineReviews/Views/GlyphMarginGrid.xaml b/src/GitHub.InlineReviews/Views/GlyphMarginGrid.xaml new file mode 100644 index 0000000000..5c8097f256 --- /dev/null +++ b/src/GitHub.InlineReviews/Views/GlyphMarginGrid.xaml @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/src/GitHub.InlineReviews/Views/GlyphMarginGrid.xaml.cs b/src/GitHub.InlineReviews/Views/GlyphMarginGrid.xaml.cs new file mode 100644 index 0000000000..251d92ef27 --- /dev/null +++ b/src/GitHub.InlineReviews/Views/GlyphMarginGrid.xaml.cs @@ -0,0 +1,16 @@ +using System; +using System.Windows.Controls; + +namespace GitHub.InlineReviews.Views +{ + /// + /// Interaction logic for GlyphMarginGrid.xaml + /// + public partial class GlyphMarginGrid : Grid + { + public GlyphMarginGrid() + { + InitializeComponent(); + } + } +} diff --git a/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml b/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml new file mode 100644 index 0000000000..c27c40447b --- /dev/null +++ b/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + You must commit and push your changes to add a comment here. + + + + + + + + + + + + + + + + + + + diff --git a/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml.cs b/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml.cs new file mode 100644 index 0000000000..77e1511a6d --- /dev/null +++ b/src/GitHub.InlineReviews/Views/InlineCommentPeekView.xaml.cs @@ -0,0 +1,31 @@ +using System; +using System.Reactive.Subjects; +using System.Windows.Controls; +using GitHub.VisualStudio.UI.Helpers; + +namespace GitHub.InlineReviews.Views +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")] + public partial class InlineCommentPeekView : UserControl + { + readonly Subject desiredHeight; + + public InlineCommentPeekView() + { + InitializeComponent(); + + desiredHeight = new Subject(); + threadView.LayoutUpdated += ThreadViewLayoutUpdated; + threadScroller.PreviewMouseWheel += ScrollViewerUtilities.FixMouseWheelScroll; + } + + public IObservable DesiredHeight => desiredHeight; + + void ThreadViewLayoutUpdated(object sender, EventArgs e) + { + var otherControlsHeight = ActualHeight - threadScroller.ActualHeight; + var threadViewHeight = threadView.DesiredSize.Height + threadView.Margin.Top + threadView.Margin.Bottom; + desiredHeight.OnNext(threadViewHeight + otherControlsHeight); + } + } +} diff --git a/src/GitHub.InlineReviews/Views/PullRequestFileMarginView.xaml b/src/GitHub.InlineReviews/Views/PullRequestFileMarginView.xaml new file mode 100644 index 0000000000..c6a0543ea9 --- /dev/null +++ b/src/GitHub.InlineReviews/Views/PullRequestFileMarginView.xaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GitHub.InlineReviews/Views/PullRequestFileMarginView.xaml.cs b/src/GitHub.InlineReviews/Views/PullRequestFileMarginView.xaml.cs new file mode 100644 index 0000000000..9dda80ff8b --- /dev/null +++ b/src/GitHub.InlineReviews/Views/PullRequestFileMarginView.xaml.cs @@ -0,0 +1,13 @@ +using System; +using System.Windows.Controls; + +namespace GitHub.InlineReviews.Views +{ + public partial class PullRequestFileMarginView : UserControl + { + public PullRequestFileMarginView() + { + InitializeComponent(); + } + } +} diff --git a/src/GitHub.InlineReviews/Views/PullRequestStatusView.xaml b/src/GitHub.InlineReviews/Views/PullRequestStatusView.xaml new file mode 100644 index 0000000000..180ecd8532 --- /dev/null +++ b/src/GitHub.InlineReviews/Views/PullRequestStatusView.xaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + View, Checkout or Create a Pull request + + + + + + + + + # - + + + + + 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.InlineReviews/VisualStudioExtensions.cs b/src/GitHub.InlineReviews/VisualStudioExtensions.cs new file mode 100644 index 0000000000..e2d1b9f5c5 --- /dev/null +++ b/src/GitHub.InlineReviews/VisualStudioExtensions.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Utilities; + +namespace GitHub.InlineReviews +{ + static class VisualStudioExtensions + { + public static T GetOptionValue(this IEditorOptions options, string optionId, T defaultValue) + { + return options.IsOptionDefined(optionId, false) ? + options.GetOptionValue(optionId) : defaultValue; + } + + public static T GetProperty(this PropertyCollection properties, object key, T defaultValue) + { + T value; + return properties.TryGetProperty(key, out value) ? value : defaultValue; + } + } +} diff --git a/src/GitHub.InlineReviews/packages.config b/src/GitHub.InlineReviews/packages.config new file mode 100644 index 0000000000..44d71eb082 --- /dev/null +++ b/src/GitHub.InlineReviews/packages.config @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.InlineReviews/source.extension.vsixmanifest b/src/GitHub.InlineReviews/source.extension.vsixmanifest new file mode 100644 index 0000000000..72e62bba15 --- /dev/null +++ b/src/GitHub.InlineReviews/source.extension.vsixmanifest @@ -0,0 +1,19 @@ + + + + + GitHub Inline Reviews + Inline reviews for GitHub pull requests + + + + + + + + + + + + + diff --git a/src/GitHub.Logging/GitHub.Logging.csproj b/src/GitHub.Logging/GitHub.Logging.csproj new file mode 100644 index 0000000000..952eef5f75 --- /dev/null +++ b/src/GitHub.Logging/GitHub.Logging.csproj @@ -0,0 +1,100 @@ + + + + + Debug + AnyCPU + {8D73575A-A89F-47CC-B153-B47DD06837F0} + Library + Properties + GitHub + GitHub.Logging + 7.3 + v4.6.1 + 512 + + ..\common\GitHubVS.ruleset + true + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + true + + + true + full + false + CODE_ANALYSIS;DEBUG;TRACE + prompt + 4 + true + bin\Debug\ + + + + + ..\..\packages\Serilog.2.5.0\lib\net46\Serilog.dll + True + + + ..\..\packages\Serilog.Enrichers.Process.2.0.1\lib\net45\Serilog.Enrichers.Process.dll + True + + + ..\..\packages\Serilog.Enrichers.Thread.3.0.0\lib\net45\Serilog.Enrichers.Thread.dll + True + + + ..\..\packages\Serilog.Sinks.File.3.2.0\lib\net45\Serilog.Sinks.File.dll + True + + + + + + + + + + + + + Properties\SolutionInfo.cs + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.Logging/Info/ApplicationInfo.cs b/src/GitHub.Logging/Info/ApplicationInfo.cs new file mode 100644 index 0000000000..7134aad155 --- /dev/null +++ b/src/GitHub.Logging/Info/ApplicationInfo.cs @@ -0,0 +1,40 @@ +using System; +using System.Diagnostics; + +namespace GitHub.Info +{ + public static class ApplicationInfo + { +#if DEBUG + public const string ApplicationName = "GìtHūbVisualStudio"; + public const string ApplicationProvider = "GitHub"; +#else + public const string ApplicationName = "GitHubVisualStudio"; + public const string ApplicationProvider = "GitHub"; +#endif + public const string ApplicationSafeName = "GitHubVisualStudio"; + public const string ApplicationDescription = "GitHub Extension for Visual Studio"; + + /// + /// Gets the version information for the host process. + /// + /// The version of the host process. + public static FileVersionInfo GetHostVersionInfo() + { + return Process.GetCurrentProcess().MainModule.FileVersionInfo; + } + + /// + /// Gets the version of a Visual Studio package. + /// + /// + /// The VS Package object. This is untyped here as this assembly does not depend on + /// any VS assemblies. + /// + /// The version of the package. + public static Version GetPackageVersion(object package) + { + return package.GetType().Assembly.GetName().Version; + } + } +} diff --git a/src/GitHub.Logging/Logging/ILoggerExtensions.cs b/src/GitHub.Logging/Logging/ILoggerExtensions.cs new file mode 100644 index 0000000000..3d61316ab2 --- /dev/null +++ b/src/GitHub.Logging/Logging/ILoggerExtensions.cs @@ -0,0 +1,29 @@ +using Serilog; + +namespace GitHub.Logging +{ + public static class ILoggerExtensions + { + public static void Assert(this ILogger logger, bool condition, string messageTemplate) + { + if (!condition) + { + messageTemplate = "Assertion Failed: " + messageTemplate; +#pragma warning disable Serilog004 // propertyValues might not be strings + logger.Warning(messageTemplate); +#pragma warning restore Serilog004 + } + } + + public static void Assert(this ILogger logger, bool condition, string messageTemplate, params object[] propertyValues) + { + if (!condition) + { + messageTemplate = "Assertion Failed: " + messageTemplate; +#pragma warning disable Serilog004 // propertyValues might not be strings + logger.Warning(messageTemplate, propertyValues); +#pragma warning restore Serilog004 + } + } + } +} \ No newline at end of file diff --git a/src/GitHub.Logging/Logging/Log.cs b/src/GitHub.Logging/Logging/Log.cs new file mode 100644 index 0000000000..721f528391 --- /dev/null +++ b/src/GitHub.Logging/Logging/Log.cs @@ -0,0 +1,13 @@ +using System; +using Serilog; + +namespace GitHub.Logging +{ + public static class Log + { + private static Lazy Logger { get; } = new Lazy(() => LogManager.ForContext(typeof(Log))); + + public static void Assert(bool condition, string messageTemplate) + => Logger.Value.Assert(condition, messageTemplate); + } +} \ No newline at end of file diff --git a/src/GitHub.Logging/Logging/LogManager.cs b/src/GitHub.Logging/Logging/LogManager.cs new file mode 100644 index 0000000000..f6212e5490 --- /dev/null +++ b/src/GitHub.Logging/Logging/LogManager.cs @@ -0,0 +1,59 @@ +using System; +using System.IO; +using System.Diagnostics.CodeAnalysis; +using GitHub.Info; +using Serilog; +using Serilog.Core; +using Serilog.Events; + +namespace GitHub.Logging +{ + public static class LogManager + { +#if DEBUG + private static LogEventLevel DefaultLoggingLevel = LogEventLevel.Debug; +#else + private static LogEventLevel DefaultLoggingLevel = LogEventLevel.Information; +#endif + + private static LoggingLevelSwitch LoggingLevelSwitch = new LoggingLevelSwitch(DefaultLoggingLevel); + + static Logger CreateLogger() + { + var logPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + ApplicationInfo.ApplicationName, + "extension.log"); + + const string outputTemplate = + "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{ProcessId:00000}] {Level:u4} [{ThreadId:00}] {ShortSourceContext,-25} {Message:lj}{NewLine}{Exception}"; + + return new LoggerConfiguration() + .Enrich.WithProcessId() + .Enrich.WithThreadId() + .MinimumLevel.ControlledBy(LoggingLevelSwitch) + .WriteTo.File(logPath, + fileSizeLimitBytes: null, + outputTemplate: outputTemplate, + shared: true) + .CreateLogger(); + } + + public static void EnableTraceLogging(bool enable) + { + var logEventLevel = enable ? LogEventLevel.Verbose : DefaultLoggingLevel; + if(LoggingLevelSwitch.MinimumLevel != logEventLevel) + { + ForContext(typeof(LogManager)).Information("Set Logging Level: {LogEventLevel}", logEventLevel); + LoggingLevelSwitch.MinimumLevel = logEventLevel; + } + } + + static Lazy Logger { get; } = new Lazy(CreateLogger); + + [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")] + public static ILogger ForContext() => ForContext(typeof(T)); + + public static ILogger ForContext(Type type) => Logger.Value.ForContext(type).ForContext("ShortSourceContext", type.Name); + } +} \ No newline at end of file diff --git a/src/GitHub.Logging/Properties/AssemblyInfo.cs b/src/GitHub.Logging/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..748cab9da4 --- /dev/null +++ b/src/GitHub.Logging/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Reflection; + +[assembly: AssemblyTitle("GitHub.Logging")] +[assembly: AssemblyDescription("")] \ No newline at end of file diff --git a/src/GitHub.Logging/packages.config b/src/GitHub.Logging/packages.config new file mode 100644 index 0000000000..faa0a313a0 --- /dev/null +++ b/src/GitHub.Logging/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.Services.Vssdk/Commands/MenuCommandServiceExtensions.cs b/src/GitHub.Services.Vssdk/Commands/MenuCommandServiceExtensions.cs new file mode 100644 index 0000000000..f06dcc7d0f --- /dev/null +++ b/src/GitHub.Services.Vssdk/Commands/MenuCommandServiceExtensions.cs @@ -0,0 +1,100 @@ +using System; +using System.ComponentModel.Design; +using System.Windows.Input; +using GitHub.Commands; +using GitHub.Extensions; +using Microsoft.VisualStudio.Shell; + +namespace GitHub.Services.Vssdk.Commands +{ + /// + /// Extension methods for . + /// + public static class MenuCommandServiceExtensions + { + /// + /// Adds s or s to a menu. + /// + /// The menu command service. + /// The commands to add. + public static void AddCommands( + this IMenuCommandService service, + params IVsCommandBase[] commands) + { + Guard.ArgumentNotNull(service, nameof(service)); + Guard.ArgumentNotNull(commands, nameof(commands)); + + foreach (MenuCommand command in commands) + { + service.AddCommand(command); + } + } + + /// + /// Binds an to a Visual Studio command. + /// + /// The menu command service. + /// The ID of the visual studio command. + /// The to bind + /// + /// If true, the visual studio command will be hidden when disabled. + /// + /// + /// This method wires up the to be executed when the Visual Studio + /// command is invoked, and for the 's + /// state to control the enabled/visible state of + /// the Visual Studio command. + /// + /// + /// The created . + /// + public static OleMenuCommand BindCommand( + this IMenuCommandService service, + CommandID id, + ICommand command, + bool hideWhenDisabled = false) + { + Guard.ArgumentNotNull(service, nameof(service)); + Guard.ArgumentNotNull(id, nameof(id)); + Guard.ArgumentNotNull(command, nameof(command)); + + var bound = new BoundCommand(id, command, hideWhenDisabled); + service.AddCommand(bound); + return bound; + } + + class BoundCommand : OleMenuCommand + { + readonly ICommand inner; + readonly bool hideWhenDisabled; + + public BoundCommand(CommandID id, ICommand command, bool hideWhenDisabled) + : base(InvokeHandler, delegate { }, HandleBeforeQueryStatus, id) + { + Guard.ArgumentNotNull(id, nameof(id)); + Guard.ArgumentNotNull(command, nameof(command)); + + inner = command; + this.hideWhenDisabled = hideWhenDisabled; + inner.CanExecuteChanged += (s, e) => HandleBeforeQueryStatus(this, e); + } + + static void InvokeHandler(object sender, EventArgs e) + { + var command = sender as BoundCommand; + command?.inner.Execute((e as OleMenuCmdEventArgs)?.InValue); + } + + static void HandleBeforeQueryStatus(object sender, EventArgs e) + { + var command = sender as BoundCommand; + + if (command != null) + { + command.Enabled = command.inner.CanExecute(null); + command.Visible = command.hideWhenDisabled ? command.Enabled : true; + } + } + } + } +} diff --git a/src/GitHub.Services.Vssdk/Commands/VsCommand.cs b/src/GitHub.Services.Vssdk/Commands/VsCommand.cs new file mode 100644 index 0000000000..0c3e3da031 --- /dev/null +++ b/src/GitHub.Services.Vssdk/Commands/VsCommand.cs @@ -0,0 +1,85 @@ +using System; +using System.Windows.Input; +using GitHub.Commands; +using GitHub.Extensions; +using Task = System.Threading.Tasks.Task; + +namespace GitHub.Services.Vssdk.Commands +{ + /// + /// Implements for s that don't accept a + /// parameter. + /// + /// + /// + /// This class derives from and implements + /// so that the command can be bound in the UI. + /// + /// + /// To implement a new command, inherit from this class and override the + /// method to provide the implementation of the command. + /// + /// + public abstract class VsCommand : VsCommandBase, IVsCommand + { + /// + /// Initializes a new instance of the class. + /// + /// The GUID of the group the command belongs to. + /// The numeric identifier of the command. + protected VsCommand(Guid commandSet, int commandId) + : base(commandSet, commandId) + { + } + + /// + /// Overridden by derived classes with the implementation of the command. + /// + /// A task that tracks the execution of the command. + public abstract Task Execute(); + + /// + protected sealed override void ExecuteUntyped(object parameter) + { + Execute().Forget(); + } + } + + /// + /// Implements for s that accept a parameter. + /// + /// The type of the parameter accepted by the command. + /// + /// This class derives from and implements + /// so that the command can be bound in the UI. + /// + /// + /// To implement a new command, inherit from this class and override the + /// method to provide the implementation of the command. + /// + public abstract class VsCommand : VsCommandBase, IVsCommand, ICommand + { + /// + /// Initializes a new instance of the class. + /// + /// The GUID of the group the command belongs to. + /// The numeric identifier of the command. + protected VsCommand(Guid commandSet, int commandId) + : base(commandSet, commandId) + { + } + + /// + /// Overridden by derived classes with the implementation of the command. + /// + /// /// The command parameter. + /// A task that tracks the execution of the command. + public abstract Task Execute(TParam parameter); + + /// + protected sealed override void ExecuteUntyped(object parameter) + { + Execute((TParam)parameter).Forget(); + } + } +} diff --git a/src/GitHub.Services.Vssdk/Commands/VsCommandBase.cs b/src/GitHub.Services.Vssdk/Commands/VsCommandBase.cs new file mode 100644 index 0000000000..cca02d90e8 --- /dev/null +++ b/src/GitHub.Services.Vssdk/Commands/VsCommandBase.cs @@ -0,0 +1,76 @@ +using System; +using System.ComponentModel.Design; +using System.Windows.Input; +using GitHub.Commands; +using Microsoft.VisualStudio.Shell; + +namespace GitHub.Services.Vssdk.Commands +{ + /// + /// Base class for and . + /// + public abstract class VsCommandBase : OleMenuCommand, IVsCommandBase + { + EventHandler canExecuteChanged; + + /// + /// Initializes a new instance of the class. + /// + /// The GUID of the group the command belongs to. + /// The numeric identifier of the command. + protected VsCommandBase(Guid commandSet, int commandId) + : base(ExecHandler, delegate { }, QueryStatusHandler, new CommandID(commandSet, commandId)) + { + } + + /// + event EventHandler ICommand.CanExecuteChanged + { + add { canExecuteChanged += value; } + remove { canExecuteChanged -= value; } + } + + /// + bool ICommand.CanExecute(object parameter) + { + QueryStatus(); + return Enabled && Visible; + } + + /// + void ICommand.Execute(object parameter) + { + ExecuteUntyped(parameter); + } + + /// + /// When overridden in a derived class, executes the command after casting the passed + /// parameter to the correct type. + /// + /// The parameter + protected abstract void ExecuteUntyped(object parameter); + + protected override void OnCommandChanged(EventArgs e) + { + base.OnCommandChanged(e); + canExecuteChanged?.Invoke(this, e); + } + + protected virtual void QueryStatus() + { + } + + static void ExecHandler(object sender, EventArgs e) + { + var args = (OleMenuCmdEventArgs)e; + var command = sender as VsCommandBase; + command?.ExecuteUntyped(args.InValue); + } + + static void QueryStatusHandler(object sender, EventArgs e) + { + var command = sender as VsCommandBase; + command?.QueryStatus(); + } + } +} diff --git a/src/GitHub.Services.Vssdk/GitHub.Services.Vssdk.csproj b/src/GitHub.Services.Vssdk/GitHub.Services.Vssdk.csproj new file mode 100644 index 0000000000..b580a56a0a --- /dev/null +++ b/src/GitHub.Services.Vssdk/GitHub.Services.Vssdk.csproj @@ -0,0 +1,161 @@ + + + + + Debug + AnyCPU + {2D3D2834-33BE-45CA-B3CC-12F853557D7B} + Library + Properties + GitHub.Services.Vssdk + GitHub.Services.Vssdk + 7.3 + v4.6.1 + 512 + ..\common\GitHubVS.ruleset + true + true + + + true + full + false + DEBUG;TRACE + prompt + 4 + false + bin\Debug\ + + + true + full + false + CODE_ANALYSIS;DEBUG;TRACE + prompt + 4 + true + bin\Debug\ + + + pdbonly + true + TRACE + prompt + 4 + true + bin\Release\ + + + + ..\..\packages\Microsoft.VisualStudio.Imaging.14.3.25407\lib\net45\Microsoft.VisualStudio.Imaging.dll + True + + + True + ..\..\packages\Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime.14.3.25407\lib\Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime.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 + + + ..\..\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.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 + + + True + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.10.0.10.0.30319\lib\Microsoft.VisualStudio.Shell.Interop.10.0.dll + True + + + True + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.11.0.11.0.61030\lib\Microsoft.VisualStudio.Shell.Interop.11.0.dll + True + + + True + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.12.0.12.0.30110\lib\Microsoft.VisualStudio.Shell.Interop.12.0.dll + True + + + True + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.14.0.DesignTime.14.3.25407\lib\Microsoft.VisualStudio.Shell.Interop.14.0.DesignTime.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.8.0.8.0.50727\lib\Microsoft.VisualStudio.Shell.Interop.8.0.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.9.0.9.0.30729\lib\Microsoft.VisualStudio.Shell.Interop.9.0.dll + True + + + ..\..\packages\Microsoft.VisualStudio.TextManager.Interop.7.10.6070\lib\Microsoft.VisualStudio.TextManager.Interop.dll + True + + + ..\..\packages\Microsoft.VisualStudio.TextManager.Interop.8.0.8.0.50727\lib\Microsoft.VisualStudio.TextManager.Interop.8.0.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Threading.14.1.111\lib\net45\Microsoft.VisualStudio.Threading.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Utilities.14.3.25407\lib\net45\Microsoft.VisualStudio.Utilities.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Validation.14.1.111\lib\net45\Microsoft.VisualStudio.Validation.dll + True + + + + + + + + + Properties\SolutionInfo.cs + + + + + + + + + {9AEA02DB-02B5-409C-B0CA-115D05331A6B} + GitHub.Exports + + + {6afe2e2d-6db0-4430-a2ea-f5f5388d2f78} + GitHub.Extensions + + + + + + \ No newline at end of file diff --git a/src/GitHub.Services.Vssdk/Properties/AssemblyInfo.cs b/src/GitHub.Services.Vssdk/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..08f1d22c43 --- /dev/null +++ b/src/GitHub.Services.Vssdk/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Reflection; + +[assembly: AssemblyTitle("GitHub.Services.Vssdk")] +[assembly: AssemblyDescription("Abstractions for the VSSDK")] \ No newline at end of file diff --git a/src/GitHub.Services.Vssdk/packages.config b/src/GitHub.Services.Vssdk/packages.config new file mode 100644 index 0000000000..8ec99b891b --- /dev/null +++ b/src/GitHub.Services.Vssdk/packages.config @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.StartPage/GitHub.StartPage.csproj b/src/GitHub.StartPage/GitHub.StartPage.csproj new file mode 100644 index 0000000000..74a2246009 --- /dev/null +++ b/src/GitHub.StartPage/GitHub.StartPage.csproj @@ -0,0 +1,216 @@ + + + + + + + $(VisualStudioVersion) + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + true + + + + + Debug + AnyCPU + 2.0 + {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + {50E277B8-8580-487A-8F8E-5C3B9FBF0F77} + Library + Properties + GitHub.StartPage + GitHub.StartPage + 7.3 + v4.6.1 + true + true + true + true + true + true + ..\common\GitHubVS.ruleset + true + true + False + False + + + true + full + false + TRACE;DEBUG + prompt + 4 + false + bin\Debug\ + + + true + full + false + TRACE;DEBUG;CODE_ANALYSIS + prompt + 4 + true + bin\Debug\ + + + pdbonly + true + TRACE + prompt + 4 + true + bin\Release\ + + + + Properties\SolutionInfo.cs + + + + + + + Designer + + + Designer + + + + + {1ce2d235-8072-4649-ba5a-cfb1af8776e0} + ReactiveUI_Net45 + + + {e4ed0537-d1d9-44b6-9212-3096d7c3f7a1} + GitHub.Exports.Reactive + + + {9aea02db-02b5-409c-b0ca-115d05331a6b} + GitHub.Exports + + + {8d73575a-a89f-47cc-b153-b47dd06837f0} + GitHub.Logging + + + {346384dd-2445-4a28-af22-b45f3957bd89} + GitHub.UI + + + + + ..\..\lib\15.0\Microsoft.TeamFoundation.Controls.dll + + + ..\..\lib\15.0\Microsoft.TeamFoundation.Git.Controls.dll + + + ..\..\packages\Microsoft.VisualStudio.CoreUtility.15.0.25901-RC\lib\net45\Microsoft.VisualStudio.CoreUtility.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Imaging.15.0.25901-RC\lib\net45\Microsoft.VisualStudio.Imaging.dll + True + + + ..\..\packages\Microsoft.VisualStudio.OLE.Interop.7.10.6070\lib\Microsoft.VisualStudio.OLE.Interop.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Shell.15.0.15.0.25901-RC\lib\Microsoft.VisualStudio.Shell.15.0.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Shell.Framework.15.0.25901-RC\lib\net45\Microsoft.VisualStudio.Shell.Framework.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Shell.Immutable.10.0.10.0.30319\lib\net40\Microsoft.VisualStudio.Shell.Immutable.10.0.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.7.10.6071\lib\Microsoft.VisualStudio.Shell.Interop.dll + True + + + True + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.10.0.10.0.30319\lib\Microsoft.VisualStudio.Shell.Interop.10.0.dll + True + + + True + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.11.0.11.0.61030\lib\Microsoft.VisualStudio.Shell.Interop.11.0.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.8.0.8.0.50727\lib\Microsoft.VisualStudio.Shell.Interop.8.0.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.9.0.9.0.30729\lib\Microsoft.VisualStudio.Shell.Interop.9.0.dll + True + + + ..\..\packages\Microsoft.VisualStudio.TextManager.Interop.7.10.6070\lib\Microsoft.VisualStudio.TextManager.Interop.dll + True + + + ..\..\packages\Microsoft.VisualStudio.TextManager.Interop.8.0.8.0.50727\lib\Microsoft.VisualStudio.TextManager.Interop.8.0.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Threading.15.0.20-pre\lib\net45\Microsoft.VisualStudio.Threading.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Utilities.15.0.25901-RC\lib\net45\Microsoft.VisualStudio.Utilities.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Validation.15.0.11-pre\lib\net45\Microsoft.VisualStudio.Validation.dll + True + + + ..\..\packages\Serilog.2.5.0\lib\net46\Serilog.dll + True + + + + ..\..\packages\Rx-Core.2.2.5-custom\lib\net45\System.Reactive.Core.dll + True + + + ..\..\packages\Rx-Interfaces.2.2.5-custom\lib\net45\System.Reactive.Interfaces.dll + True + + + + + + + + + + + + + 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}. + + + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.StartPage/Properties/AssemblyInfo.cs b/src/GitHub.StartPage/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..ba001558de --- /dev/null +++ b/src/GitHub.StartPage/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Reflection; + +[assembly: AssemblyTitle("GitHub.StartPage")] +[assembly: AssemblyDescription("GitHub Start Page for Visual Studio")] diff --git a/src/GitHub.StartPage/Resources.resx b/src/GitHub.StartPage/Resources.resx new file mode 100644 index 0000000000..c10fe53dd0 --- /dev/null +++ b/src/GitHub.StartPage/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + GitHub + + + Powerful collaboration, code review, and code management for open source and private projects. + + \ No newline at end of file diff --git a/src/GitHub.StartPage/StartPagePackage.cs b/src/GitHub.StartPage/StartPagePackage.cs new file mode 100644 index 0000000000..5635052a08 --- /dev/null +++ b/src/GitHub.StartPage/StartPagePackage.cs @@ -0,0 +1,163 @@ +using System; +using System.ComponentModel; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using GitHub.Logging; +using GitHub.Models; +using GitHub.Primitives; +using GitHub.Services; +using GitHub.VisualStudio; +using Microsoft.TeamFoundation.Controls; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.CodeContainerManagement; +using Microsoft.VisualStudio.Threading; +using Serilog; +using CodeContainer = Microsoft.VisualStudio.Shell.CodeContainerManagement.CodeContainer; +using ICodeContainerProvider = Microsoft.VisualStudio.Shell.CodeContainerManagement.ICodeContainerProvider; +using Task = System.Threading.Tasks.Task; + +namespace GitHub.StartPage +{ + [PackageRegistration(UseManagedResourcesOnly = true)] + [Guid(Guids.StartPagePackageId)] + [ProvideCodeContainerProvider("GitHub Container", Guids.StartPagePackageId, Guids.ImagesId, 1, "#110", "#111", typeof(GitHubContainerProvider))] + public sealed class StartPagePackage : ExtensionPointPackage + { + static IServiceProvider serviceProvider; + internal static IServiceProvider ServiceProvider { get { return serviceProvider; } } + + public StartPagePackage() + { + serviceProvider = this; + } + } + + [Guid(Guids.CodeContainerProviderId)] + public class GitHubContainerProvider : ICodeContainerProvider + { + static readonly ILogger log = LogManager.ForContext(); + + public async Task AcquireCodeContainerAsync(IProgress downloadProgress, CancellationToken cancellationToken) + { + + return await RunAcquisition(downloadProgress, cancellationToken, null); + } + + public async Task AcquireCodeContainerAsync(RemoteCodeContainer onlineCodeContainer, IProgress downloadProgress, CancellationToken cancellationToken) + { + var repository = new RepositoryModel(onlineCodeContainer.Name, UriString.ToUriString(onlineCodeContainer.DisplayUrl)); + return await RunAcquisition(downloadProgress, cancellationToken, repository); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "cancellationToken")] + async Task RunAcquisition(IProgress downloadProgress, CancellationToken cancellationToken, IRepositoryModel repository) + { + CloneDialogResult request = null; + + try + { + var uiProvider = await Task.Run(() => Package.GetGlobalService(typeof(IGitHubServiceProvider)) as IGitHubServiceProvider); + await ShowTeamExplorerPage(uiProvider); + request = await ShowCloneDialog(uiProvider, downloadProgress, repository); + } + catch (Exception e) + { + log.Error(e, "Error showing Start Page clone dialog"); + } + + if (request == null) + return null; + + var path = Path.Combine(request.BasePath, request.Repository.Name); + var uri = request.Repository.CloneUrl.ToRepositoryUrl(); + return new CodeContainer( + localProperties: new CodeContainerLocalProperties(path, CodeContainerType.Folder, + new CodeContainerSourceControlProperties(request.Repository.Name, path, new Guid(Guids.GitSccProviderId))), + remote: new RemoteCodeContainer(request.Repository.Name, + new Guid(Guids.CodeContainerProviderId), + uri, + new Uri(uri.ToString().TrimSuffix(".git")), + DateTimeOffset.UtcNow), + isFavorite: false, + lastAccessed: DateTimeOffset.UtcNow); + } + + async Task ShowTeamExplorerPage(IGitHubServiceProvider gitHubServiceProvider) + { + var te = gitHubServiceProvider?.GetService(typeof(ITeamExplorer)) as ITeamExplorer; + + if (te != null) + { + var page = te.NavigateToPage(new Guid(TeamExplorerPageIds.Connect), null); + + if (page == null) + { + var tcs = new TaskCompletionSource(); + PropertyChangedEventHandler handler = null; + + handler = new PropertyChangedEventHandler((s, e) => + { + if (e.PropertyName == "CurrentPage") + { + tcs.SetResult(te.CurrentPage); + te.PropertyChanged -= handler; + } + }); + + te.PropertyChanged += handler; + + page = await tcs.Task; + } + } + } + + async Task ShowCloneDialog( + IGitHubServiceProvider gitHubServiceProvider, + IProgress progress, + IRepositoryModel repository = null) + { + var dialogService = gitHubServiceProvider.GetService(); + var cloneService = gitHubServiceProvider.GetService(); + var usageTracker = gitHubServiceProvider.GetService(); + CloneDialogResult result = null; + + if (repository == null) + { + result = await dialogService.ShowCloneDialog(null); + } + else + { + var basePath = await dialogService.ShowReCloneDialog(repository); + + if (basePath != null) + { + result = new CloneDialogResult(basePath, repository); + } + } + + if (result != null) + { + try + { + await cloneService.CloneRepository( + result.Repository.CloneUrl, + result.Repository.Name, + result.BasePath, + progress); + + usageTracker.IncrementCounter(x => x.NumberOfStartPageClones).Forget(); + } + catch + { + var teServices = gitHubServiceProvider.TryGetService(); + teServices.ShowError($"Failed to clone the repository '{result.Repository.Name}'"); + result = null; + } + } + + return result; + } + } +} diff --git a/src/GitHub.StartPage/packages.config b/src/GitHub.StartPage/packages.config new file mode 100644 index 0000000000..4f783e3a26 --- /dev/null +++ b/src/GitHub.StartPage/packages.config @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.StartPage/source.extension.vsixmanifest b/src/GitHub.StartPage/source.extension.vsixmanifest new file mode 100644 index 0000000000..ab6b44c127 --- /dev/null +++ b/src/GitHub.StartPage/source.extension.vsixmanifest @@ -0,0 +1,21 @@ + + + + + GitHub Start Page + GitHub on your Start Page, helping you clone! + + + + + + + + + + + + + + + diff --git a/src/GitHub.TeamFoundation.14/Base/EnsureLoggedInSection.cs b/src/GitHub.TeamFoundation.14/Base/EnsureLoggedInSection.cs new file mode 100644 index 0000000000..4a8322cca2 --- /dev/null +++ b/src/GitHub.TeamFoundation.14/Base/EnsureLoggedInSection.cs @@ -0,0 +1,65 @@ +using System; +using System.Globalization; +using System.Reactive.Linq; +using System.Threading.Tasks; +using GitHub.Api; +using GitHub.Extensions; +using GitHub.Primitives; +using GitHub.Services; +using GitHub.VisualStudio.Base; +using GitHub.VisualStudio.UI; + +namespace GitHub.VisualStudio.TeamExplorer.Sync +{ + public class EnsureLoggedInSection : TeamExplorerSectionBase + { + readonly ITeamExplorerServices teServices; + readonly IDialogService dialogService; + + public EnsureLoggedInSection(IGitHubServiceProvider serviceProvider, + ISimpleApiClientFactory apiFactory, ITeamExplorerServiceHolder holder, + IConnectionManager cm, ITeamExplorerServices teServices, + IDialogService dialogService) + : base(serviceProvider, apiFactory, holder, cm) + { + IsVisible = false; + this.teServices = teServices; + this.dialogService = dialogService; + } + + public override void Initialize(IServiceProvider serviceProvider) + { + base.Initialize(serviceProvider); + CheckLogin().Forget(); + } + + protected override void RepoChanged(bool changed) + { + base.RepoChanged(changed); + CheckLogin().Forget(); + } + + async Task CheckLogin() + { + // this is not a github repo, or it hasn't been published yet + if (ActiveRepo == null || ActiveRepoUri == null) + return; + + var isgithub = await IsAGitHubRepo(); + if (!isgithub) + return; + + teServices.ClearNotifications(); + var add = HostAddress.Create(ActiveRepoUri); + bool loggedIn = await connectionManager.IsLoggedIn(add); + if (!loggedIn) + { + var msg = string.Format(CultureInfo.CurrentUICulture, Resources.NotLoggedInMessage, add.Title, add.Title); + teServices.ShowMessage( + msg, + new Primitives.RelayCommand(_ => dialogService.ShowLoginDialog()) + ); + } + } + } +} \ No newline at end of file diff --git a/src/GitHub.VisualStudio/Base/TeamExplorerGitAwareItemBase.cs b/src/GitHub.TeamFoundation.14/Base/TeamExplorerGitAwareItemBase.cs similarity index 100% rename from src/GitHub.VisualStudio/Base/TeamExplorerGitAwareItemBase.cs rename to src/GitHub.TeamFoundation.14/Base/TeamExplorerGitAwareItemBase.cs diff --git a/src/GitHub.VisualStudio/Base/TeamExplorerInvitationBase.cs b/src/GitHub.TeamFoundation.14/Base/TeamExplorerInvitationBase.cs similarity index 84% rename from src/GitHub.VisualStudio/Base/TeamExplorerInvitationBase.cs rename to src/GitHub.TeamFoundation.14/Base/TeamExplorerInvitationBase.cs index 9457d043f1..424e29ca1e 100644 --- a/src/GitHub.VisualStudio/Base/TeamExplorerInvitationBase.cs +++ b/src/GitHub.TeamFoundation.14/Base/TeamExplorerInvitationBase.cs @@ -1,7 +1,8 @@ -using GitHub.VisualStudio.Helpers; +using GitHub.Services; +using GitHub.VisualStudio.Helpers; using Microsoft.TeamFoundation.Controls; -using NullGuard; using System; +using GitHub.Extensions; namespace GitHub.VisualStudio.Base { @@ -9,9 +10,14 @@ public class TeamExplorerInvitationBase : TeamExplorerBase, ITeamExplorerService { public static readonly Guid TeamExplorerInvitationSectionGuid = new Guid("8914ac06-d960-4537-8345-cb13c00378d8"); + protected TeamExplorerInvitationBase(IGitHubServiceProvider serviceProvider) : base(serviceProvider) + {} + public virtual void Initialize(IServiceProvider serviceProvider) { - ServiceProvider = serviceProvider; + Guard.ArgumentNotNull(serviceProvider, nameof(serviceProvider)); + + TEServiceProvider = serviceProvider; } /// @@ -40,28 +46,22 @@ public bool CanSignUp } string connectLabel; - [AllowNull] public string ConnectLabel { - [return: AllowNull] get { return connectLabel; } set { connectLabel = value; this.RaisePropertyChange(); } } string description; - [AllowNull] public string Description { - [return: AllowNull] get { return description; } set { description = value; this.RaisePropertyChange(); } } object icon; - [AllowNull] public object Icon { - [return: AllowNull] get { return icon; } set { icon = value; this.RaisePropertyChange(); } } @@ -74,28 +74,22 @@ public bool IsVisible } string name; - [AllowNull] public string Name { - [return: AllowNull] get { return name; } set { name = value; this.RaisePropertyChange(); } } string provider; - [AllowNull] public string Provider { - [return: AllowNull] get { return provider; } set { provider = value; this.RaisePropertyChange(); } } string signUpLabel; - [AllowNull] public string SignUpLabel { - [return: AllowNull] get { return signUpLabel; } set { signUpLabel = value; this.RaisePropertyChange(); } } diff --git a/src/GitHub.TeamFoundation.14/Base/TeamExplorerNavigationItemBase.cs b/src/GitHub.TeamFoundation.14/Base/TeamExplorerNavigationItemBase.cs new file mode 100644 index 0000000000..f8b73819db --- /dev/null +++ b/src/GitHub.TeamFoundation.14/Base/TeamExplorerNavigationItemBase.cs @@ -0,0 +1,116 @@ +using System; +using System.Diagnostics; +using System.Drawing; +using GitHub.Api; +using GitHub.Extensions; +using GitHub.Services; +using GitHub.UI; +using GitHub.VisualStudio.Helpers; +using Microsoft.TeamFoundation.Controls; +using Microsoft.VisualStudio.PlatformUI; +using GitHub.Models; + +namespace GitHub.VisualStudio.Base +{ + public class TeamExplorerNavigationItemBase : TeamExplorerItemBase, ITeamExplorerNavigationItem2 + { + readonly Octicon octicon; + + public TeamExplorerNavigationItemBase(IGitHubServiceProvider serviceProvider, + ISimpleApiClientFactory apiFactory, ITeamExplorerServiceHolder holder, Octicon octicon) + : base(serviceProvider, apiFactory, holder) + { + Guard.ArgumentNotNull(serviceProvider, nameof(serviceProvider)); + Guard.ArgumentNotNull(apiFactory, nameof(apiFactory)); + Guard.ArgumentNotNull(holder, nameof(holder)); + + this.octicon = octicon; + + IsVisible = false; + IsEnabled = true; + + OnThemeChanged(); + VSColorTheme.ThemeChanged += _ => + { + OnThemeChanged(); + Invalidate(); + }; + + holder.Subscribe(this, UpdateRepo); + } + + public override async void Invalidate() + { + IsVisible = false; + IsVisible = await IsAGitHubRepo(); + } + + void OnThemeChanged() + { + var theme = Colors.DetectTheme(); + var dark = theme == "Dark"; + Icon = SharedResources.GetDrawingForIcon(octicon, dark ? Colors.DarkThemeNavigationItem : Colors.LightThemeNavigationItem, theme); + } + + void UpdateRepo(ILocalRepositoryModel repo) + { + var changed = ActiveRepo != repo; + ActiveRepo = repo; + RepoChanged(changed); + Invalidate(); + } + + protected void OpenInBrowser(Lazy browser, string endpoint) + { + var uri = ActiveRepoUri; + Debug.Assert(uri != null, "OpenInBrowser: uri should never be null"); +#if !DEBUG + if (uri == null) + return; +#endif + var browseUrl = uri.ToRepositoryUrl().Append(endpoint); + + OpenInBrowser(browser, browseUrl); + } + + void Unsubscribe() + { + holder.Unsubscribe(this); + } + + bool disposed; + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (!disposed) + { + Unsubscribe(); + disposed = true; + } + } + base.Dispose(disposing); + } + + int argbColor; + public int ArgbColor + { + get { return argbColor; } + set { argbColor = value; this.RaisePropertyChange(); } + } + + object icon; + public object Icon + { + get { return icon; } + set { icon = value; this.RaisePropertyChange(); } + } + + Image image; + public Image Image + { + get{ return image; } + set { image = value; this.RaisePropertyChange(); } + } + } +} diff --git a/src/GitHub.TeamFoundation.14/Base/TeamExplorerSectionBase.cs b/src/GitHub.TeamFoundation.14/Base/TeamExplorerSectionBase.cs new file mode 100644 index 0000000000..7d53af6001 --- /dev/null +++ b/src/GitHub.TeamFoundation.14/Base/TeamExplorerSectionBase.cs @@ -0,0 +1,125 @@ +using System; +using GitHub.VisualStudio.Helpers; +using Microsoft.TeamFoundation.Controls; +using GitHub.Services; +using System.Diagnostics; +using GitHub.Api; +using GitHub.Models; +using GitHub.ViewModels; +using GitHub.Extensions; + +namespace GitHub.VisualStudio.Base +{ + public class TeamExplorerSectionBase : TeamExplorerItemBase, ITeamExplorerSection, IServiceProviderAware + { + protected IConnectionManager connectionManager; + + bool isBusy; + public bool IsBusy + { + get { return isBusy; } + set { isBusy = value; this.RaisePropertyChange(); } + } + + bool isExpanded; + public bool IsExpanded + { + get { return isExpanded; } + set { isExpanded = value; this.RaisePropertyChange(); } + } + + object sectionContent; + public object SectionContent + { + get { return sectionContent; } + set { sectionContent = value; this.RaisePropertyChange(); } + } + + string title; + public string Title + { + get { return title; } + set { title = value; this.RaisePropertyChange(); } + } + + public virtual object GetExtensibilityService(Type serviceType) + { + return null; + } + + public TeamExplorerSectionBase(IGitHubServiceProvider serviceProvider, ITeamExplorerServiceHolder holder) + : base(serviceProvider, holder) + { + Guard.ArgumentNotNull(serviceProvider, nameof(serviceProvider)); + Guard.ArgumentNotNull(holder, nameof(holder)); + + IsVisible = false; + IsEnabled = true; + IsExpanded = true; + } + + public TeamExplorerSectionBase(IGitHubServiceProvider serviceProvider, + ISimpleApiClientFactory apiFactory, ITeamExplorerServiceHolder holder) + : base(serviceProvider, apiFactory, holder) + { + Guard.ArgumentNotNull(serviceProvider, nameof(serviceProvider)); + Guard.ArgumentNotNull(apiFactory, nameof(apiFactory)); + Guard.ArgumentNotNull(holder, nameof(holder)); + + IsVisible = false; + IsEnabled = true; + IsExpanded = true; + } + + public TeamExplorerSectionBase(IGitHubServiceProvider serviceProvider, + ITeamExplorerServiceHolder holder, IConnectionManager cm) : this(serviceProvider, holder) + { + Guard.ArgumentNotNull(serviceProvider, nameof(serviceProvider)); + Guard.ArgumentNotNull(holder, nameof(holder)); + Guard.ArgumentNotNull(cm, nameof(cm)); + + connectionManager = cm; + } + + public TeamExplorerSectionBase(IGitHubServiceProvider serviceProvider, + ISimpleApiClientFactory apiFactory, ITeamExplorerServiceHolder holder, + IConnectionManager cm) : this(serviceProvider, apiFactory, holder) + { + Guard.ArgumentNotNull(serviceProvider, nameof(serviceProvider)); + Guard.ArgumentNotNull(apiFactory, nameof(apiFactory)); + Guard.ArgumentNotNull(holder, nameof(holder)); + Guard.ArgumentNotNull(cm, nameof(cm)); + + connectionManager = cm; + } + + void ITeamExplorerSection.Cancel() + { + } + + void ITeamExplorerSection.Initialize(object sender, SectionInitializeEventArgs e) + { + Guard.ArgumentNotNull(e, nameof(e)); + + Initialize(e.ServiceProvider); + } + + public virtual void Loaded(object sender, SectionLoadedEventArgs e) + { + } + + public virtual void Refresh() + { + } + + public virtual void SaveContext(object sender, SectionSaveContextEventArgs e) + { + } + + protected ITeamExplorerSection GetSection(Guid section) + { + var tep = (ITeamExplorerPage)TEServiceProvider.GetServiceSafe(typeof(ITeamExplorerPage)); + return tep?.GetSection(section); + } + } +} diff --git a/src/GitHub.TeamFoundation.14/Base/TeamExplorerServiceHolder.cs b/src/GitHub.TeamFoundation.14/Base/TeamExplorerServiceHolder.cs new file mode 100644 index 0000000000..20879c8fbe --- /dev/null +++ b/src/GitHub.TeamFoundation.14/Base/TeamExplorerServiceHolder.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using GitHub.Extensions; +using GitHub.Logging; +using GitHub.Models; +using GitHub.Services; +using Serilog; +using Microsoft.TeamFoundation.Controls; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Threading; +using System.Windows; + +namespace GitHub.VisualStudio.Base +{ + [Export(typeof(ITeamExplorerServiceHolder))] + [PartCreationPolicy(CreationPolicy.Shared)] + public class TeamExplorerServiceHolder : ITeamExplorerServiceHolder + { + static readonly ILogger log = LogManager.ForContext(); + + readonly Dictionary> activeRepoHandlers = new Dictionary>(); + ILocalRepositoryModel activeRepo; + bool activeRepoNotified = false; + + IServiceProvider serviceProvider; + readonly IVSGitExt gitService; + + /// + /// This class relies on IVSGitExt that provides information when VS switches repositories. + /// + /// Used for monitoring the active repository. + [ImportingConstructor] + TeamExplorerServiceHolder(IVSGitExt gitService) : this(gitService, ThreadHelper.JoinableTaskContext) + { + } + + /// + /// This constructor can be used for unit testing. + /// + /// Used for monitoring the active repository. + /// Used for switching to the Main thread. + public TeamExplorerServiceHolder(IVSGitExt gitService, JoinableTaskContext joinableTaskContext) + { + JoinableTaskCollection = joinableTaskContext.CreateCollection(); + JoinableTaskCollection.DisplayName = nameof(TeamExplorerServiceHolder); + JoinableTaskFactory = joinableTaskContext.CreateFactory(JoinableTaskCollection); + + // This might be null in Blend or SafeMode + if (gitService != null) + { + this.gitService = gitService; + UpdateActiveRepo(); + gitService.ActiveRepositoriesChanged += UpdateActiveRepo; + } + } + + // set by the sections when they get initialized + public IServiceProvider ServiceProvider + { + get { return serviceProvider; } + set + { + if (serviceProvider == value) + return; + + serviceProvider = value; + if (serviceProvider == null) + return; + } + } + + public ILocalRepositoryModel ActiveRepo + { + get { return activeRepo; } + private set + { + if (activeRepo == value) + return; + if (activeRepo != null) + activeRepo.PropertyChanged -= ActiveRepoPropertyChanged; + activeRepo = value; + if (activeRepo != null) + activeRepo.PropertyChanged += ActiveRepoPropertyChanged; + NotifyActiveRepo(); + } + } + + public void Subscribe(object who, Action handler) + { + Guard.ArgumentNotNull(who, nameof(who)); + Guard.ArgumentNotNull(handler, nameof(handler)); + + bool notificationsExist; + ILocalRepositoryModel repo; + lock (activeRepoHandlers) + { + repo = ActiveRepo; + notificationsExist = activeRepoNotified; + if (!activeRepoHandlers.ContainsKey(who)) + activeRepoHandlers.Add(who, handler); + else + activeRepoHandlers[who] = handler; + } + + // the repo url might have changed and we don't get notifications + // for that, so this is a good place to refresh it in case that happened + repo?.Refresh(); + + // if the active repo hasn't changed and there's notifications queued up, + // notify the subscriber. If the repo has changed, the set above will trigger + // notifications so we don't have to do it here. + if (repo == ActiveRepo && notificationsExist) + handler(repo); + } + + public void Unsubscribe(object who) + { + Guard.ArgumentNotNull(who, nameof(who)); + + if (activeRepoHandlers.ContainsKey(who)) + activeRepoHandlers.Remove(who); + } + + /// + /// Clears the current ServiceProvider if it matches the one that is passed in. + /// This is usually called on Dispose, which might happen after another section + /// has changed the ServiceProvider to something else, which is why we require + /// the parameter to match. + /// + /// If the current ServiceProvider matches this, clear it + public void ClearServiceProvider(IServiceProvider provider) + { + Guard.ArgumentNotNull(provider, nameof(provider)); + + if (serviceProvider != provider) + return; + + ServiceProvider = null; + } + + void NotifyActiveRepo() + { + lock (activeRepoHandlers) + { + activeRepoNotified = true; + foreach (var handler in activeRepoHandlers.Values) + handler(activeRepo); + } + } + + void UpdateActiveRepo() + { + var repo = gitService.ActiveRepositories.FirstOrDefault(); + + if (!Equals(repo, ActiveRepo)) + { + // Fire property change events on Main thread + JoinableTaskFactory.RunAsync(async () => + { + await JoinableTaskFactory.SwitchToMainThreadAsync(); + ActiveRepo = repo; + }).Task.Forget(log); + } + } + + void ActiveRepoPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + Guard.ArgumentNotNull(e, nameof(e)); + + if (e.PropertyName == "CloneUrl") + ActiveRepo = sender as ILocalRepositoryModel; + } + + public IGitAwareItem HomeSection + { + get + { + if (ServiceProvider == null) + return null; + var page = PageService; + if (page == null) + return null; + return page.GetSection(new Guid(TeamExplorer.Home.GitHubHomeSection.GitHubHomeSectionId)) as IGitAwareItem; + } + } + + ITeamExplorerPage PageService + { + get { return ServiceProvider.GetServiceSafe(); } + } + + public JoinableTaskCollection JoinableTaskCollection { get; } + JoinableTaskFactory JoinableTaskFactory { get; } + } +} diff --git a/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection.cs b/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection.cs new file mode 100644 index 0000000000..451fd141b9 --- /dev/null +++ b/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection.cs @@ -0,0 +1,576 @@ +using System; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Threading.Tasks; +using System.Windows.Input; +using GitHub.Api; +using GitHub.Extensions; +using GitHub.Logging; +using GitHub.Models; +using GitHub.Primitives; +using GitHub.Services; +using GitHub.Settings; +using GitHub.UI; +using GitHub.VisualStudio.Base; +using GitHub.VisualStudio.Helpers; +using GitHub.VisualStudio.UI; +using GitHub.VisualStudio.UI.Views; +using Microsoft.TeamFoundation.Controls; +using Microsoft.VisualStudio; +using ReactiveUI; +using Serilog; + +namespace GitHub.VisualStudio.TeamExplorer.Connect +{ + public class GitHubConnectSection : TeamExplorerSectionBase, IGitHubConnectSection + { + static readonly ILogger log = LogManager.ForContext(); + readonly IPackageSettings packageSettings; + readonly IVSServices vsServices; + readonly int sectionIndex; + readonly ILocalRepositories localRepositories; + readonly IUsageTracker usageTracker; + + ITeamExplorerSection invitationSection; + string errorMessage; + bool isCloning; + bool isCreating; + GitHubConnectSectionState settings; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")] + SectionStateTracker sectionTracker; + + protected GitHubConnectContent View + { + get { return SectionContent as GitHubConnectContent; } + private set { SectionContent = value; } + } + + public string ErrorMessage + { + get { return errorMessage; } + private set { errorMessage = value; this.RaisePropertyChange(); } + } + + public IConnection SectionConnection { get; set; } + + bool isLoggingIn; + public bool IsLoggingIn + { + get { return isLoggingIn; } + private set { isLoggingIn = value; this.RaisePropertyChange(); } + } + + bool showLogin; + public bool ShowLogin + { + get { return showLogin; } + private set { showLogin = value; this.RaisePropertyChange(); } + } + + bool showLogout; + public bool ShowLogout + { + get { return showLogout; } + private set { showLogout = value; this.RaisePropertyChange(); } + } + + bool showRetry; + public bool ShowRetry + { + get { return showRetry; } + private set { showRetry = value; this.RaisePropertyChange(); } + } + + IReactiveDerivedList repositories; + public IReactiveDerivedList Repositories + { + get { return repositories; } + private set { repositories = value; this.RaisePropertyChange(); } + } + + ILocalRepositoryModel selectedRepository; + public ILocalRepositoryModel SelectedRepository + { + get { return selectedRepository; } + set { selectedRepository = value; this.RaisePropertyChange(); } + } + + public ICommand Clone { get; } + + internal ITeamExplorerServiceHolder Holder => holder; + + public GitHubConnectSection(IGitHubServiceProvider serviceProvider, + ISimpleApiClientFactory apiFactory, + ITeamExplorerServiceHolder holder, + IConnectionManager manager, + IPackageSettings packageSettings, + IVSServices vsServices, + ILocalRepositories localRepositories, + IUsageTracker usageTracker, + int index) + : base(serviceProvider, apiFactory, holder, manager) + { + Guard.ArgumentNotNull(apiFactory, nameof(apiFactory)); + Guard.ArgumentNotNull(holder, nameof(holder)); + Guard.ArgumentNotNull(manager, nameof(manager)); + Guard.ArgumentNotNull(packageSettings, nameof(packageSettings)); + Guard.ArgumentNotNull(vsServices, nameof(vsServices)); + Guard.ArgumentNotNull(localRepositories, nameof(localRepositories)); + Guard.ArgumentNotNull(usageTracker, nameof(usageTracker)); + + Title = "GitHub"; + IsEnabled = true; + IsVisible = false; + sectionIndex = index; + + this.packageSettings = packageSettings; + this.vsServices = vsServices; + this.localRepositories = localRepositories; + this.usageTracker = usageTracker; + + Clone = CreateAsyncCommandHack(DoClone); + + connectionManager.Connections.CollectionChanged += RefreshConnections; + PropertyChanged += OnPropertyChange; + UpdateConnection(); + } + + async Task DoClone() + { + var dialogService = ServiceProvider.GetService(); + var result = await dialogService.ShowCloneDialog(SectionConnection); + + if (result != null) + { + try + { + ServiceProvider.GitServiceProvider = TEServiceProvider; + var cloneService = ServiceProvider.GetService(); + await cloneService.CloneRepository( + result.Repository.CloneUrl, + result.Repository.Name, + result.BasePath); + + usageTracker.IncrementCounter(x => x.NumberOfGitHubConnectSectionClones).Forget(); + } + catch (Exception e) + { + var teServices = ServiceProvider.TryGetService(); + teServices.ShowError(e.GetUserFriendlyErrorMessage(ErrorType.ClonedFailed, result.Repository.Name)); + } + } + } + + void RefreshConnections(object sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + if (connectionManager.Connections.Count > sectionIndex) + Refresh(connectionManager.Connections[sectionIndex]); + break; + case NotifyCollectionChangedAction.Remove: + Refresh(connectionManager.Connections.Count <= sectionIndex + ? null + : connectionManager.Connections[sectionIndex]); + break; + } + } + + protected void Refresh(IConnection connection) + { + InitializeInvitationSection(); + + ErrorMessage = connection?.ConnectionError?.GetUserFriendlyErrorMessage(ErrorType.LoginFailed); + IsLoggingIn = connection?.IsLoggingIn ?? false; + IsVisible = connection != null || (invitationSection?.IsVisible == false); + + if (connection == null || !connection.IsLoggedIn) + { + if (Repositories != null) + Repositories.CollectionChanged -= UpdateRepositoryList; + Repositories = null; + settings = null; + + if (connection?.ConnectionError != null) + { + ShowLogin = false; + ShowLogout = true; + ShowRetry = !(connection.ConnectionError is Octokit.AuthorizationException); + } + else + { + ShowLogin = true; + ShowLogout = false; + ShowRetry = false; + } + } + else if (connection != SectionConnection || Repositories == null) + { + Repositories?.Dispose(); + Repositories = localRepositories.GetRepositoriesForAddress(connection.HostAddress); + Repositories.CollectionChanged += UpdateRepositoryList; + settings = packageSettings.UIState.GetOrCreateConnectSection(Title); + ShowLogin = false; + ShowLogout = true; + Title = connection.HostAddress.Title; + } + + if (connection != null && TEServiceProvider != null) + { + RefreshRepositories().Forget(); + } + + if (SectionConnection != connection) + { + if (SectionConnection != null) + { + SectionConnection.PropertyChanged -= ConnectionPropertyChanged; + } + + SectionConnection = connection; + + if (SectionConnection != null) + { + SectionConnection.PropertyChanged += ConnectionPropertyChanged; + } + } + } + + public override void Refresh() + { + UpdateConnection(); + base.Refresh(); + } + + public override void Initialize(IServiceProvider serviceProvider) + { + Guard.ArgumentNotNull(serviceProvider, nameof(serviceProvider)); + + base.Initialize(serviceProvider); + UpdateConnection(); + + // watch for new repos added to the local repo list + var section = GetSection(TeamExplorerConnectionsSectionId); + if (section != null) + sectionTracker = new SectionStateTracker(section, RefreshRepositories); + } + + void InitializeInvitationSection() + { + // We're only interested in the invitation section if sectionIndex == 0. Don't want to show + // two "Log In" options. + if (sectionIndex == 0 && invitationSection == null) + { + invitationSection = GetSection(TeamExplorerInvitationBase.TeamExplorerInvitationSectionGuid); + + if (invitationSection != null) + { + invitationSection.PropertyChanged += InvitationSectionPropertyChanged; + } + } + } + + void UpdateConnection() + { + Refresh(connectionManager.Connections.Count > sectionIndex + ? connectionManager.Connections[sectionIndex] + : SectionConnection); + } + + void OnPropertyChange(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(IsVisible) && IsVisible && View == null) + View = new GitHubConnectContent { DataContext = this }; + else if (e.PropertyName == nameof(IsExpanded) && settings != null) + settings.IsExpanded = IsExpanded; + } + + void InvitationSectionPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(ITeamExplorerSection.IsVisible)) + { + Refresh(SectionConnection); + } + } + + private void ConnectionPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(IConnection.IsLoggedIn) || + e.PropertyName == nameof(IConnection.IsLoggingIn)) + { + Refresh(SectionConnection); + } + } + + async void UpdateRepositoryList(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + // if we're cloning or creating, only one repo will be added to the list + // so we can handle just one new entry separately + if (isCloning || isCreating) + { + var newrepo = e.NewItems.Cast().First(); + + SelectedRepository = newrepo; + if (isCreating) + HandleCreatedRepo(newrepo); + else + HandleClonedRepo(newrepo); + + isCreating = isCloning = false; + + try + { + // TODO: Cache the icon state. + var api = await ApiFactory.Create(newrepo.CloneUrl); + var repo = await api.GetRepository(); + newrepo.SetIcon(repo.Private, repo.Fork); + } + catch (Exception ex) + { + // GetRepository() may throw if the user doesn't have permissions to access the repo + // (because the repo no longer exists, or because the user has logged in on a different + // profile, or their permissions have changed remotely) + log.Error(ex, "Error updating repository list"); + } + } + // looks like it's just a refresh with new stuff on the list, update the icons + else + { + e.NewItems + .Cast() + .ForEach(async r => + { + if (Equals(Holder.ActiveRepo, r)) + SelectedRepository = r; + + try + { + // TODO: Cache the icon state. + var api = await ApiFactory.Create(r.CloneUrl); + var repo = await api.GetRepository(); + r.SetIcon(repo.Private, repo.Fork); + } + catch (Exception ex) + { + // GetRepository() may throw if the user doesn't have permissions to access the repo + // (because the repo no longer exists, or because the user has logged in on a different + // profile, or their permissions have changed remotely) + log.Error(ex, "Error updating repository list"); + } + }); + } + } + } + + void HandleCreatedRepo(ILocalRepositoryModel newrepo) + { + Guard.ArgumentNotNull(newrepo, nameof(newrepo)); + + var msg = string.Format(CultureInfo.CurrentCulture, Constants.Notification_RepoCreated, newrepo.Name, newrepo.CloneUrl); + msg += " " + string.Format(CultureInfo.CurrentCulture, Constants.Notification_CreateNewProject, newrepo.LocalPath); + ShowNotification(newrepo, msg); + } + + void HandleClonedRepo(ILocalRepositoryModel newrepo) + { + Guard.ArgumentNotNull(newrepo, nameof(newrepo)); + + var msg = string.Format(CultureInfo.CurrentCulture, Constants.Notification_RepoCloned, newrepo.Name, newrepo.CloneUrl); + if (newrepo.HasCommits() && newrepo.MightContainSolution()) + msg += " " + string.Format(CultureInfo.CurrentCulture, Constants.Notification_OpenProject, newrepo.LocalPath); + else + msg += " " + string.Format(CultureInfo.CurrentCulture, Constants.Notification_CreateNewProject, newrepo.LocalPath); + ShowNotification(newrepo, msg); + } + + void ShowNotification(ILocalRepositoryModel newrepo, string msg) + { + Guard.ArgumentNotNull(newrepo, nameof(newrepo)); + + var teServices = ServiceProvider.TryGetService(); + + teServices.ClearNotifications(); + teServices.ShowMessage( + msg, + new RelayCommand(o => + { + var str = o.ToString(); + /* the prefix is the action to perform: + * u: launch browser with url + * c: launch create new project dialog + * o: launch open existing project dialog + */ + var prefix = str.Substring(0, 2); + if (prefix == "u:") + OpenInBrowser(ServiceProvider.TryGetService(), new Uri(str.Substring(2))); + else if (prefix == "o:") + { + if (ErrorHandler.Succeeded(ServiceProvider.GetSolution().OpenSolutionViaDlg(str.Substring(2), 1))) + ServiceProvider.TryGetService()?.NavigateToPage(new Guid(TeamExplorerPageIds.Home), null); + } + else if (prefix == "c:") + { + var vsGitServices = ServiceProvider.TryGetService(); + vsGitServices.SetDefaultProjectPath(newrepo.LocalPath); + if (ErrorHandler.Succeeded(ServiceProvider.GetSolution().CreateNewProjectViaDlg(null, null, 0))) + ServiceProvider.TryGetService()?.NavigateToPage(new Guid(TeamExplorerPageIds.Home), null); + } + }) + ); + log.Debug("Notification"); + } + + async Task RefreshRepositories() + { + // TODO: This is wasteful as we can be calling it multiple times for a single changed + // signal, once from each section. Needs refactoring. + await localRepositories.Refresh(); + RaisePropertyChanged("Repositories"); // trigger a re-check of the visibility of the listview based on item count + } + + public void DoCreate() + { + ServiceProvider.GitServiceProvider = TEServiceProvider; + var dialogService = ServiceProvider.GetService(); + dialogService.ShowCreateRepositoryDialog(SectionConnection); + } + + public void SignOut() + { + connectionManager.LogOut(SectionConnection.HostAddress); + } + + public void Login() + { + var dialogService = ServiceProvider.GetService(); + dialogService.ShowLoginDialog(); + } + + public void Retry() + { + connectionManager.Retry(SectionConnection); + } + + public bool OpenRepository() + { + var old = Repositories.FirstOrDefault(x => x.Equals(Holder.ActiveRepo)); + if (!Equals(SelectedRepository, old)) + { + var opened = vsServices.TryOpenRepository(SelectedRepository.LocalPath); + if (!opened) + { + // TryOpenRepository might fail because dir no longer exists. Let user find solution themselves. + opened = ErrorHandler.Succeeded(ServiceProvider.GetSolution().OpenSolutionViaDlg(SelectedRepository.LocalPath, 1)); + if (!opened) + { + return false; + } + } + } + + // Navigate away when we're on the correct source control contexts. + ServiceProvider.TryGetService()?.NavigateToPage(new Guid(TeamExplorerPageIds.Home), null); + return true; + } + + bool disposed; + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (!disposed) + { + connectionManager.Connections.CollectionChanged -= RefreshConnections; + if (Repositories != null) + Repositories.CollectionChanged -= UpdateRepositoryList; + disposed = true; + packageSettings.Save(); + } + } + base.Dispose(disposing); + } + + /// + /// Creates a ReactiveCommand that works like a command created via + /// but that does not hang when the async + /// task shows a modal dialog. + /// + /// Method that creates the task to run. + /// A reactive command. + /// + /// The command needs to be disabled while a clone operation is in + /// progress but also needs to display a modal dialog. For some reason using + /// causes a weird UI hang in this situation + /// where the UI runs but WhenAny no longer responds to property changed notifications. + /// + static ReactiveCommand CreateAsyncCommandHack(Func executeAsync) + { + Guard.ArgumentNotNull(executeAsync, nameof(executeAsync)); + + var enabled = new BehaviorSubject(true); + var command = ReactiveCommand.Create(enabled); + command.Subscribe(async _ => + { + enabled.OnNext(false); + try { await executeAsync(); } + finally { enabled.OnNext(true); } + }); + return command; + } + + class SectionStateTracker + { + enum SectionState + { + Idle, + Busy, + Refreshing + } + + readonly Stateless.StateMachine machine; + readonly ITeamExplorerSection section; + + public SectionStateTracker(ITeamExplorerSection section, Func onRefreshed) + { + this.section = section; + machine = new Stateless.StateMachine(SectionState.Idle); + machine.Configure(SectionState.Idle) + .PermitIf("IsBusy", SectionState.Busy, () => this.section.IsBusy) + .IgnoreIf("IsBusy", () => !this.section.IsBusy); + machine.Configure(SectionState.Busy) + .Permit("Title", SectionState.Refreshing) + .PermitIf("IsBusy", SectionState.Idle, () => !this.section.IsBusy) + .IgnoreIf("IsBusy", () => this.section.IsBusy); + machine.Configure(SectionState.Refreshing) + .Ignore("Title") + .PermitIf("IsBusy", SectionState.Idle, () => !this.section.IsBusy) + .IgnoreIf("IsBusy", () => this.section.IsBusy) + .OnExit(() => onRefreshed()); + + section.PropertyChanged += TrackState; + } +#if DEBUG + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")] +#endif + void TrackState(object sender, PropertyChangedEventArgs e) + { + if (machine.PermittedTriggers.Contains(e.PropertyName)) + { + log.Debug("{PropertyName} title:{Title} busy:{IsBusy}", + e.PropertyName, + ((ITeamExplorerSection)sender).Title, + ((ITeamExplorerSection)sender).IsBusy); + machine.Fire(e.PropertyName); + } + } + } + } +} diff --git a/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection0.cs b/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection0.cs new file mode 100644 index 0000000000..3822195cc3 --- /dev/null +++ b/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection0.cs @@ -0,0 +1,28 @@ +using GitHub.Api; +using GitHub.Services; +using GitHub.Settings; +using Microsoft.TeamFoundation.Controls; +using System.ComponentModel.Composition; + +namespace GitHub.VisualStudio.TeamExplorer.Connect +{ + [TeamExplorerSection(GitHubConnectSection0Id, TeamExplorerPageIds.Connect, 10)] + [PartCreationPolicy(CreationPolicy.NonShared)] + public class GitHubConnectSection0 : GitHubConnectSection + { + public const string GitHubConnectSection0Id = "519B47D3-F2A9-4E19-8491-8C9FA25ABE90"; + + [ImportingConstructor] + public GitHubConnectSection0(IGitHubServiceProvider serviceProvider, + ISimpleApiClientFactory apiFactory, + ITeamExplorerServiceHolder holder, + IConnectionManager manager, + IPackageSettings settings, + IVSServices vsServices, + ILocalRepositories localRepositories, + IUsageTracker usageTracker) + : base(serviceProvider, apiFactory, holder, manager, settings, vsServices, localRepositories, usageTracker, 0) + { + } + } +} diff --git a/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection1.cs b/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection1.cs new file mode 100644 index 0000000000..4534940642 --- /dev/null +++ b/src/GitHub.TeamFoundation.14/Connect/GitHubConnectSection1.cs @@ -0,0 +1,28 @@ +using GitHub.Api; +using GitHub.Services; +using GitHub.Settings; +using Microsoft.TeamFoundation.Controls; +using System.ComponentModel.Composition; + +namespace GitHub.VisualStudio.TeamExplorer.Connect +{ + [TeamExplorerSection(GitHubConnectSection1Id, TeamExplorerPageIds.Connect, 10)] + [PartCreationPolicy(CreationPolicy.NonShared)] + public class GitHubConnectSection1 : GitHubConnectSection + { + public const string GitHubConnectSection1Id = "519B47D3-F2A9-4E19-8491-8C9FA25ABE91"; + + [ImportingConstructor] + public GitHubConnectSection1(IGitHubServiceProvider serviceProvider, + ISimpleApiClientFactory apiFactory, + ITeamExplorerServiceHolder holder, + IConnectionManager manager, + IPackageSettings settings, + IVSServices vsServices, + ILocalRepositories localRepositories, + IUsageTracker usageTracker) + : base(serviceProvider, apiFactory, holder, manager, settings, vsServices, localRepositories, usageTracker, 1) + { + } + } +} diff --git a/src/GitHub.TeamFoundation.14/Connect/GitHubInvitationSection.cs b/src/GitHub.TeamFoundation.14/Connect/GitHubInvitationSection.cs new file mode 100644 index 0000000000..de7d12d43b --- /dev/null +++ b/src/GitHub.TeamFoundation.14/Connect/GitHubInvitationSection.cs @@ -0,0 +1,72 @@ +using GitHub.Info; +using GitHub.Models; +using GitHub.Services; +using GitHub.UI; +using GitHub.VisualStudio.Base; +using GitHub.Extensions; +using Microsoft.TeamFoundation.Controls; +using Microsoft.VisualStudio.PlatformUI; +using System; +using System.ComponentModel.Composition; +using System.Windows; +using System.Windows.Media; +using GitHub.VisualStudio.UI; +using System.Linq; + +namespace GitHub.VisualStudio.TeamExplorer.Connect +{ + [TeamExplorerServiceInvitation(GitHubInvitationSectionId, GitHubInvitationSectionPriority)] + [PartCreationPolicy(CreationPolicy.NonShared)] + public class GitHubInvitationSection : TeamExplorerInvitationBase + { + public const string GitHubInvitationSectionId = "C2443FCC-6D62-4D31-B08A-C4DE70109C7F"; + public const int GitHubInvitationSectionPriority = 100; + readonly IDialogService dialogService; + readonly Lazy lazyBrowser; + + [ImportingConstructor] + public GitHubInvitationSection( + IGitHubServiceProvider serviceProvider, + IDialogService dialogService, + IConnectionManager cm, + Lazy browser) + : base(serviceProvider) + { + this.dialogService = dialogService; + lazyBrowser = browser; + CanConnect = true; + CanSignUp = true; + ConnectLabel = Resources.GitHubInvitationSectionConnectLabel; + SignUpLabel = Resources.SignUpLink; + Name = "GitHub"; + Provider = "GitHub, Inc."; + Description = Resources.BlurbText; + OnThemeChanged(); + VSColorTheme.ThemeChanged += _ => + { + OnThemeChanged(); + }; + + IsVisible = cm.Connections.Count == 0; + + cm.Connections.CollectionChanged += (s, e) => IsVisible = cm.Connections.Count == 0; + } + + public override void Connect() + { + dialogService.ShowLoginDialog(); + } + + public override void SignUp() + { + OpenInBrowser(lazyBrowser, GitHubUrls.Plans); + } + + void OnThemeChanged() + { + var theme = Helpers.Colors.DetectTheme(); + var dark = theme == "Dark"; + Icon = SharedResources.GetDrawingForIcon(Octicon.mark_github, dark ? Colors.White : Helpers.Colors.LightThemeNavigationItem, theme); + } + } +} diff --git a/src/GitHub.TeamFoundation.14/GitHub.TeamFoundation.14.csproj b/src/GitHub.TeamFoundation.14/GitHub.TeamFoundation.14.csproj new file mode 100644 index 0000000000..5a493885fe --- /dev/null +++ b/src/GitHub.TeamFoundation.14/GitHub.TeamFoundation.14.csproj @@ -0,0 +1,252 @@ + + + + + + $(MSBuildToolsVersion) + $(MSBuildToolsVersion) + Debug + AnyCPU + {161DBF01-1DBF-4B00-8551-C5C00F26720D} + Library + Properties + GitHub.TeamFoundation + GitHub.TeamFoundation.14 + 7.3 + v4.6.1 + 512 + ..\common\GitHubVS.ruleset + true + true + + + + + true + full + false + DEBUG;TRACE;TEAMEXPLORER14 + prompt + 4 + false + bin\Debug\ + + + true + full + false + CODE_ANALYSIS;DEBUG;TRACE;TEAMEXPLORER14 + prompt + 4 + true + bin\Debug\ + + + pdbonly + true + TRACE;TEAMEXPLORER14 + prompt + 4 + true + bin\Release\ + + + + + ..\..\packages\LibGit2Sharp.0.23.1\lib\net40\LibGit2Sharp.dll + True + + + ..\..\lib\14.0\Microsoft.TeamFoundation.Common.dll + False + + + ..\..\lib\14.0\Microsoft.TeamFoundation.Client.dll + False + + + ..\..\lib\14.0\Microsoft.TeamFoundation.Controls.dll + False + + + ..\..\lib\14.0\Microsoft.TeamFoundation.Git.Controls.dll + False + + + ..\..\lib\14.0\Microsoft.TeamFoundation.Git.Provider.dll + False + + + ..\..\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 + + + ..\..\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 + + + ..\..\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 + + + ..\..\packages\Microsoft.VisualStudio.Threading.14.1.131\lib\net45\Microsoft.VisualStudio.Threading.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Validation.14.1.111\lib\net45\Microsoft.VisualStudio.Validation.dll + True + + + + + ..\..\packages\Serilog.2.5.0\lib\net46\Serilog.dll + True + + + ..\..\packages\Stateless.2.5.56.0\lib\portable-net40+sl50+win+wp80+MonoAndroid10+xamarinios10+MonoTouch10\Stateless.dll + True + + + + + + + ..\..\packages\Rx-Core.2.2.5-custom\lib\net45\System.Reactive.Core.dll + True + + + ..\..\packages\Rx-Interfaces.2.2.5-custom\lib\net45\System.Reactive.Interfaces.dll + True + + + False + ..\..\packages\Rx-Linq.2.2.5-custom\lib\net45\System.Reactive.Linq.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Properties\SolutionInfo.cs + + + + Designer + + + + + {08dd4305-7787-4823-a53f-4d0f725a07f3} + Octokit + + + {1ce2d235-8072-4649-ba5a-cfb1af8776e0} + ReactiveUI_Net45 + + + {252ce1c2-027a-4445-a3c2-e4d6c80a935a} + Splat-Net45 + + + {b389adaf-62cc-486e-85b4-2d8b078df763} + GitHub.Api + + + {1A1DA411-8D1F-4578-80A6-04576BEA2DC5} + GitHub.App + + + {E4ED0537-D1D9-44B6-9212-3096D7C3F7A1} + GitHub.Exports.Reactive + + + {9aea02db-02b5-409c-b0ca-115d05331a6b} + GitHub.Exports + + + {6afe2e2d-6db0-4430-a2ea-f5f5388d2f78} + GitHub.Extensions + + + {8d73575a-a89f-47cc-b153-b47dd06837f0} + GitHub.Logging + + + {d1dfbb0c-b570-4302-8f1e-2e3a19c41961} + GitHub.VisualStudio.UI + + + + + + + + + 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}. + + + + + \ No newline at end of file diff --git a/src/GitHub.TeamFoundation.14/Home/ForkNavigationItem.cs b/src/GitHub.TeamFoundation.14/Home/ForkNavigationItem.cs new file mode 100644 index 0000000000..12e8424b40 --- /dev/null +++ b/src/GitHub.TeamFoundation.14/Home/ForkNavigationItem.cs @@ -0,0 +1,117 @@ +using System; +using System.ComponentModel.Composition; +using System.Reactive.Linq; +using GitHub.Api; +using GitHub.Services; +using GitHub.VisualStudio.Base; +using GitHub.VisualStudio.Helpers; +using Microsoft.TeamFoundation.Controls; +using GitHub.UI; +using GitHub.VisualStudio.UI; +using GitHub.Models; +using GitHub.Primitives; +using GitHub.Extensions; +using GitHub.Logging; +using Serilog; +using System.Collections.Specialized; +using GitHub.Settings; + +namespace GitHub.VisualStudio.TeamExplorer.Home +{ + [TeamExplorerNavigationItem(ForkNavigationItemId, NavigationItemPriority.Fork)] + [PartCreationPolicy(CreationPolicy.NonShared)] + public class ForkNavigationItem : TeamExplorerNavigationItemBase + { + static readonly ILogger log = LogManager.ForContext(); + + public const string ForkNavigationItemId = "5245767A-B657-4F8E-BFEE-F04159F1DDA6"; + + readonly IDialogService dialogService; + readonly IPackageSettings packageSettings; + readonly IUsageTracker usageTracker; + + IConnectionManager connectionManager; + + [ImportingConstructor] + public ForkNavigationItem(IGitHubServiceProvider serviceProvider, + ISimpleApiClientFactory apiFactory, + ITeamExplorerServiceHolder holder, + IDialogService dialogService, + IPackageSettings packageSettings, + IUsageTracker usageTracker) + : base(serviceProvider, apiFactory, holder, Octicon.repo_forked) + { + this.dialogService = dialogService; + this.packageSettings = packageSettings; + this.usageTracker = usageTracker; + + Text = Resources.ForkNavigationItemText; + ArgbColor = Colors.PurpleNavigationItem.ToInt32(); + ConnectionManager.Connections.CollectionChanged += ConnectionsChanged; + + packageSettings.PropertyChanged += (sender, args) => + { + if (args.PropertyName == nameof(packageSettings.ForkButton)) + { + IsVisible = packageSettings.ForkButton; + } + }; + } + + IConnectionManager ConnectionManager + { + get + { + // We can't receive IConnectionManager in the constructor because Invalidate() + // is called from the base constructor and so we don't have chance to save it + // to a field. + if (connectionManager == null) + { + connectionManager = ServiceProvider.GetService(); + } + + return connectionManager; + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + ConnectionManager.Connections.CollectionChanged -= ConnectionsChanged; + } + + public override async void Execute() + { + var connection = await connectionManager.GetConnection(ActiveRepo); + + if (connection?.IsLoggedIn == true) + { + usageTracker.IncrementCounter(model => model.NumberOfShowRepoForkDialogClicks).Forget(); + await dialogService.ShowForkDialog(ActiveRepo, connection); + } + } + + public override async void Invalidate() + { + try + { + IsVisible = false; + + if ((packageSettings?.ForkButton ?? false) && await IsAGitHubDotComRepo()) + { + var connection = await ConnectionManager.GetConnection(ActiveRepo); + IsVisible = connection?.IsLoggedIn ?? false; + } + } + catch (Exception ex) + { + log.Error(ex, "Error updating ForkNavigationItem visibility"); + } + } + + private void ConnectionsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + Invalidate(); + } + } +} diff --git a/src/GitHub.TeamFoundation.14/Home/GitHubHomeSection.cs b/src/GitHub.TeamFoundation.14/Home/GitHubHomeSection.cs new file mode 100644 index 0000000000..c93e77091d --- /dev/null +++ b/src/GitHub.TeamFoundation.14/Home/GitHubHomeSection.cs @@ -0,0 +1,188 @@ +using System; +using System.ComponentModel.Composition; +using System.Diagnostics; +using System.Windows.Input; +using GitHub.Api; +using GitHub.Extensions; +using GitHub.Info; +using GitHub.Primitives; +using GitHub.Services; +using GitHub.Settings; +using GitHub.UI; +using GitHub.VisualStudio.Base; +using GitHub.VisualStudio.Helpers; +using GitHub.VisualStudio.UI; +using GitHub.VisualStudio.UI.Views; +using Microsoft.TeamFoundation.Controls; + +namespace GitHub.VisualStudio.TeamExplorer.Home +{ + [TeamExplorerSection(GitHubHomeSectionId, TeamExplorerPageIds.Home, 10)] + [PartCreationPolicy(CreationPolicy.NonShared)] + public class GitHubHomeSection : TeamExplorerSectionBase, IGitHubHomeSection + { + public const string GitHubHomeSectionId = "72008232-2104-4FA0-A189-61B0C6F91198"; + const string TrainingUrl = "https://services.github.com/on-demand/windows/visual-studio"; + readonly static Guid welcomeMessageGuid = new Guid(Guids.TeamExplorerWelcomeMessage); + + readonly IVisualStudioBrowser visualStudioBrowser; + readonly ITeamExplorerServices teamExplorerServices; + readonly IPackageSettings settings; + readonly IUsageTracker usageTracker; + + [ImportingConstructor] + public GitHubHomeSection(IGitHubServiceProvider serviceProvider, + ISimpleApiClientFactory apiFactory, + ITeamExplorerServiceHolder holder, + IVisualStudioBrowser visualStudioBrowser, + ITeamExplorerServices teamExplorerServices, + IPackageSettings settings, + IUsageTracker usageTracker) + : base(serviceProvider, apiFactory, holder) + { + Title = "GitHub"; + View = new GitHubHomeContent(); + View.DataContext = this; + this.visualStudioBrowser = visualStudioBrowser; + this.teamExplorerServices = teamExplorerServices; + this.settings = settings; + this.usageTracker = usageTracker; + + var openOnGitHub = new RelayCommand(_ => DoOpenOnGitHub()); + OpenOnGitHub = openOnGitHub; + } + + bool IsGitToolsMessageVisible() + { + return teamExplorerServices.IsNotificationVisible(new Guid(Guids.TeamExplorerInstall3rdPartyGitTools)); + } + + protected async override void RepoChanged(bool changed) + { + IsLoggedIn = true; + IsVisible = false; + + base.RepoChanged(changed); + + IsVisible = await IsAGitHubRepo(); + + if (IsVisible) + { + RepoName = ActiveRepoName; + RepoUrl = ActiveRepoUri.ToString(); + Icon = GetIcon(false, true, false); + + // We want to display a welcome message but only if Team Explorer isn't + // already displaying the "Install 3rd Party Tools" message and the current repo is hosted on GitHub. + if (!settings.HideTeamExplorerWelcomeMessage && !IsGitToolsMessageVisible()) + { + ShowWelcomeMessage(); + } + + Debug.Assert(SimpleApiClient != null, + "If we're in this block, simpleApiClient cannot be null. It was created by UpdateStatus"); + var repo = await SimpleApiClient.GetRepository(); + Icon = GetIcon(repo.Private, true, repo.Fork); + IsLoggedIn = await IsUserAuthenticated(); + } + else + { + teamExplorerServices.HideNotification(welcomeMessageGuid); + } + } + + public override async void Refresh() + { + IsVisible = await IsAGitHubRepo(); + if (IsVisible) + { + IsLoggedIn = await IsUserAuthenticated(); + } + + base.Refresh(); + } + + static Octicon GetIcon(bool isPrivate, bool isHosted, bool isFork) + { + return !isHosted ? Octicon.device_desktop + : isPrivate ? Octicon.@lock + : isFork ? Octicon.repo_forked : Octicon.repo; + } + + public void Login() + { + var dialogService = ServiceProvider.GetService(); + dialogService.ShowLoginDialog(); + } + + void DoOpenOnGitHub() + { + visualStudioBrowser?.OpenUrl(ActiveRepo.CloneUrl.ToRepositoryUrl()); + } + + void ShowWelcomeMessage() + { + teamExplorerServices.ShowMessage( + Resources.TeamExplorerWelcomeMessage, + new RelayCommand(o => + { + var str = o.ToString(); + + switch (str) + { + case "show-training": + visualStudioBrowser.OpenUrl(new Uri(TrainingUrl)); + usageTracker.IncrementCounter(x => x.NumberOfWelcomeTrainingClicks).Forget(); + break; + case "show-docs": + visualStudioBrowser.OpenUrl(new Uri(GitHubUrls.Documentation)); + usageTracker.IncrementCounter(x => x.NumberOfWelcomeDocsClicks).Forget(); + break; + case "dont-show-again": + teamExplorerServices.HideNotification(welcomeMessageGuid); + settings.HideTeamExplorerWelcomeMessage = true; + settings.Save(); + break; + } + }), + false, + welcomeMessageGuid); + } + + protected GitHubHomeContent View + { + get { return SectionContent as GitHubHomeContent; } + set { SectionContent = value; } + } + + string repoName = String.Empty; + public string RepoName + { + get { return repoName; } + set { repoName = value; this.RaisePropertyChange(); } + } + + string repoUrl = String.Empty; + public string RepoUrl + { + get { return repoUrl; } + set { repoUrl = value; this.RaisePropertyChange(); } + } + + Octicon icon; + public Octicon Icon + { + get { return icon; } + set { icon = value; this.RaisePropertyChange(); } + } + + bool isLoggedIn; + public bool IsLoggedIn + { + get { return isLoggedIn; } + set { isLoggedIn = value; this.RaisePropertyChange(); } + } + + public ICommand OpenOnGitHub { get; } + } +} \ No newline at end of file diff --git a/src/GitHub.VisualStudio/TeamExplorer/Home/GraphsNavigationItem.cs b/src/GitHub.TeamFoundation.14/Home/GraphsNavigationItem.cs similarity index 75% rename from src/GitHub.VisualStudio/TeamExplorer/Home/GraphsNavigationItem.cs rename to src/GitHub.TeamFoundation.14/Home/GraphsNavigationItem.cs index 91076a56d1..e59fdc9f43 100644 --- a/src/GitHub.VisualStudio/TeamExplorer/Home/GraphsNavigationItem.cs +++ b/src/GitHub.TeamFoundation.14/Home/GraphsNavigationItem.cs @@ -6,6 +6,7 @@ using GitHub.VisualStudio.Helpers; using Microsoft.TeamFoundation.Controls; using GitHub.UI; +using GitHub.VisualStudio.UI; namespace GitHub.VisualStudio.TeamExplorer.Home { @@ -18,9 +19,11 @@ public class GraphsNavigationItem : TeamExplorerNavigationItemBase readonly Lazy browser; [ImportingConstructor] - public GraphsNavigationItem(ISimpleApiClientFactory apiFactory, Lazy browser, - ITeamExplorerServiceHolder holder) - : base(apiFactory, holder, Octicon.graph) + public GraphsNavigationItem(IGitHubServiceProvider serviceProvider, + ISimpleApiClientFactory apiFactory, + Lazy browser, + ITeamExplorerServiceHolder holder) + : base(serviceProvider, apiFactory, holder, Octicon.graph) { this.browser = browser; Text = Resources.GraphsNavigationItemText; diff --git a/src/GitHub.VisualStudio/TeamExplorer/Home/IssuesNavigationItem.cs b/src/GitHub.TeamFoundation.14/Home/IssuesNavigationItem.cs similarity index 80% rename from src/GitHub.VisualStudio/TeamExplorer/Home/IssuesNavigationItem.cs rename to src/GitHub.TeamFoundation.14/Home/IssuesNavigationItem.cs index 8f425b1c00..0ca6e99b98 100644 --- a/src/GitHub.VisualStudio/TeamExplorer/Home/IssuesNavigationItem.cs +++ b/src/GitHub.TeamFoundation.14/Home/IssuesNavigationItem.cs @@ -6,6 +6,7 @@ using GitHub.VisualStudio.Helpers; using Microsoft.TeamFoundation.Controls; using GitHub.UI; +using GitHub.VisualStudio.UI; namespace GitHub.VisualStudio.TeamExplorer.Home { @@ -18,9 +19,11 @@ public class IssuesNavigationItem : TeamExplorerNavigationItemBase readonly Lazy browser; [ImportingConstructor] - public IssuesNavigationItem(ISimpleApiClientFactory apiFactory, Lazy browser, - ITeamExplorerServiceHolder holder) - : base(apiFactory, holder, Octicon.issue_opened) + public IssuesNavigationItem(IGitHubServiceProvider serviceProvider, + ISimpleApiClientFactory apiFactory, + Lazy browser, + ITeamExplorerServiceHolder holder) + : base(serviceProvider, apiFactory, holder, Octicon.issue_opened) { this.browser = browser; Text = Resources.IssuesNavigationItemText; diff --git a/src/GitHub.TeamFoundation.14/Home/PullRequestsNavigationItem.cs b/src/GitHub.TeamFoundation.14/Home/PullRequestsNavigationItem.cs new file mode 100644 index 0000000000..c5a8da413f --- /dev/null +++ b/src/GitHub.TeamFoundation.14/Home/PullRequestsNavigationItem.cs @@ -0,0 +1,43 @@ +using System.ComponentModel.Composition; +using GitHub.Api; +using GitHub.Commands; +using GitHub.Extensions; +using GitHub.Services; +using GitHub.UI; +using GitHub.VisualStudio.Base; +using GitHub.VisualStudio.Helpers; +using GitHub.VisualStudio.UI; +using Microsoft.TeamFoundation.Controls; + +namespace GitHub.VisualStudio.TeamExplorer.Home +{ + [TeamExplorerNavigationItem(PullRequestsNavigationItemId, NavigationItemPriority.PullRequests)] + [PartCreationPolicy(CreationPolicy.NonShared)] + public class PullRequestsNavigationItem : TeamExplorerNavigationItemBase + { + public const string PullRequestsNavigationItemId = "5245767A-B657-4F8E-BFEE-F04159F1DDA3"; + + readonly IOpenPullRequestsCommand openPullRequests; + readonly IUsageTracker usageTracker; + + [ImportingConstructor] + public PullRequestsNavigationItem(IGitHubServiceProvider serviceProvider, + ISimpleApiClientFactory apiFactory, + ITeamExplorerServiceHolder holder, + IOpenPullRequestsCommand openPullRequests, + IUsageTracker usageTracker) + : base(serviceProvider, apiFactory, holder, Octicon.git_pull_request) + { + this.openPullRequests = openPullRequests; + this.usageTracker = usageTracker; + Text = Resources.PullRequestsNavigationItemText; + ArgbColor = Colors.RedNavigationItem.ToInt32(); + } + + public override void Execute() + { + openPullRequests.Execute(); + usageTracker.IncrementCounter(x => x.NumberOfTeamExplorerHomeOpenPullRequestList).Forget(); + } + } +} diff --git a/src/GitHub.VisualStudio/TeamExplorer/Home/PulseNavigationItem.cs b/src/GitHub.TeamFoundation.14/Home/PulseNavigationItem.cs similarity index 75% rename from src/GitHub.VisualStudio/TeamExplorer/Home/PulseNavigationItem.cs rename to src/GitHub.TeamFoundation.14/Home/PulseNavigationItem.cs index 8d308278db..45636ba925 100644 --- a/src/GitHub.VisualStudio/TeamExplorer/Home/PulseNavigationItem.cs +++ b/src/GitHub.TeamFoundation.14/Home/PulseNavigationItem.cs @@ -6,6 +6,7 @@ using GitHub.VisualStudio.Helpers; using Microsoft.TeamFoundation.Controls; using GitHub.UI; +using GitHub.VisualStudio.UI; namespace GitHub.VisualStudio.TeamExplorer.Home { @@ -18,9 +19,11 @@ public class PulseNavigationItem : TeamExplorerNavigationItemBase readonly Lazy browser; [ImportingConstructor] - public PulseNavigationItem(ISimpleApiClientFactory apiFactory, Lazy browser, - ITeamExplorerServiceHolder holder) - : base(apiFactory, holder, Octicon.pulse) + public PulseNavigationItem(IGitHubServiceProvider serviceProvider, + ISimpleApiClientFactory apiFactory, + Lazy browser, + ITeamExplorerServiceHolder holder) + : base(serviceProvider, apiFactory, holder, Octicon.pulse) { this.browser = browser; Text = Resources.PulseNavigationItemText; diff --git a/src/GitHub.VisualStudio/TeamExplorer/Home/WikiNavigationItem.cs b/src/GitHub.TeamFoundation.14/Home/WikiNavigationItem.cs similarity index 80% rename from src/GitHub.VisualStudio/TeamExplorer/Home/WikiNavigationItem.cs rename to src/GitHub.TeamFoundation.14/Home/WikiNavigationItem.cs index 5b18490965..ddac4fa924 100644 --- a/src/GitHub.VisualStudio/TeamExplorer/Home/WikiNavigationItem.cs +++ b/src/GitHub.TeamFoundation.14/Home/WikiNavigationItem.cs @@ -6,6 +6,7 @@ using GitHub.VisualStudio.Helpers; using Microsoft.TeamFoundation.Controls; using GitHub.UI; +using GitHub.VisualStudio.UI; namespace GitHub.VisualStudio.TeamExplorer.Home { @@ -18,9 +19,11 @@ public class WikiNavigationItem : TeamExplorerNavigationItemBase readonly Lazy browser; [ImportingConstructor] - public WikiNavigationItem(ISimpleApiClientFactory apiFactory, Lazy browser, - ITeamExplorerServiceHolder holder) - : base(apiFactory, holder, Octicon.book) + public WikiNavigationItem(IGitHubServiceProvider serviceProvider, + ISimpleApiClientFactory apiFactory, + Lazy browser, + ITeamExplorerServiceHolder holder) + : base(serviceProvider, apiFactory, holder, Octicon.book) { this.browser = browser; Text = Resources.WikiNavigationItemText; diff --git a/src/GitHub.TeamFoundation.14/Properties/AssemblyInfo.cs b/src/GitHub.TeamFoundation.14/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..6dc9797bbf --- /dev/null +++ b/src/GitHub.TeamFoundation.14/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("GitHub.TeamFoundation.14")] +[assembly: AssemblyDescription("GitHub TeamFoundation")] +[assembly: Guid("b389adaf-62cc-486e-85b4-2d8b078df763")] diff --git a/src/GitHub.TeamFoundation.14/RegistryHelper.cs b/src/GitHub.TeamFoundation.14/RegistryHelper.cs new file mode 100644 index 0000000000..e8e2906042 --- /dev/null +++ b/src/GitHub.TeamFoundation.14/RegistryHelper.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using GitHub.Logging; +using GitHub.Models; +using GitHub.Services; +using Microsoft.Win32; +using Serilog; + +namespace GitHub.TeamFoundation +{ + internal class RegistryHelper + { + static readonly ILogger log = LogManager.ForContext(); + const string TEGitKey = @"Software\Microsoft\VisualStudio\14.0\TeamFoundation\GitSourceControl"; + static RegistryKey OpenGitKey(string path) + { + return Microsoft.Win32.Registry.CurrentUser.OpenSubKey(TEGitKey + "\\" + path, true); + } + + internal static IEnumerable PokeTheRegistryForRepositoryList() + { + var key = OpenGitKey("Repositories"); + + if (key != null) + { + using (key) + { + return key.GetSubKeyNames().Select(x => + { + var subkey = key.OpenSubKey(x); + + if (subkey != null) + { + using (subkey) + { + try + { + var path = subkey?.GetValue("Path") as string; + if (path != null && Directory.Exists(path)) + return new LocalRepositoryModel(path, GitService.GitServiceHelper); + } + catch (Exception) + { + // no sense spamming the log, the registry might have ton of stale things we don't care about + } + + } + } + + return null; + }) + .Where(x => x != null) + .ToList(); + } + } + + return new ILocalRepositoryModel[0]; + } + + internal static string PokeTheRegistryForLocalClonePath() + { + using (var key = OpenGitKey("General")) + { + return (string)key?.GetValue("DefaultRepositoryPath", string.Empty, RegistryValueOptions.DoNotExpandEnvironmentNames); + } + } + + const string NewProjectDialogKeyPath = @"Software\Microsoft\VisualStudio\14.0\NewProjectDialog"; + const string MRUKeyPath = "MRUSettingsLocalProjectLocationEntries"; + internal static string SetDefaultProjectPath(string path) + { + var old = String.Empty; + try + { + var newProjectKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(NewProjectDialogKeyPath, true) ?? + Microsoft.Win32.Registry.CurrentUser.CreateSubKey(NewProjectDialogKeyPath); + + if (newProjectKey == null) + { + throw new GitHubLogicException( + string.Format( + CultureInfo.CurrentCulture, + "Could not open or create registry key '{0}'", + NewProjectDialogKeyPath)); + } + + using (newProjectKey) + { + var mruKey = newProjectKey.OpenSubKey(MRUKeyPath, true) ?? + Microsoft.Win32.Registry.CurrentUser.CreateSubKey(MRUKeyPath); + + if (mruKey == null) + { + throw new GitHubLogicException( + string.Format( + CultureInfo.CurrentCulture, + "Could not open or create registry key '{0}'", + MRUKeyPath)); + } + + using (mruKey) + { + // is this already the default path? bail + old = (string)mruKey.GetValue("Value0", string.Empty, RegistryValueOptions.DoNotExpandEnvironmentNames); + if (String.Equals(path.TrimEnd('\\'), old.TrimEnd('\\'), StringComparison.CurrentCultureIgnoreCase)) + return old; + + // grab the existing list of recent paths, throwing away the last one + var numEntries = (int)mruKey.GetValue("MaximumEntries", 5); + var entries = new List(numEntries); + for (int i = 0; i < numEntries - 1; i++) + { + var val = (string)mruKey.GetValue("Value" + i, String.Empty, RegistryValueOptions.DoNotExpandEnvironmentNames); + if (!String.IsNullOrEmpty(val)) + entries.Add(val); + } + + newProjectKey.SetValue("LastUsedNewProjectPath", path); + mruKey.SetValue("Value0", path); + // bump list of recent paths one entry down + for (int i = 0; i < entries.Count; i++) + mruKey.SetValue("Value" + (i + 1), entries[i]); + } + } + } + catch (Exception ex) + { + log.Error(ex, "Error setting the create project path in the registry"); + } + return old; + } + } +} diff --git a/src/GitHub.TeamFoundation.14/Services/LocalRepositoryModelFactory.cs b/src/GitHub.TeamFoundation.14/Services/LocalRepositoryModelFactory.cs new file mode 100644 index 0000000000..f6d7dc4a54 --- /dev/null +++ b/src/GitHub.TeamFoundation.14/Services/LocalRepositoryModelFactory.cs @@ -0,0 +1,13 @@ +using GitHub.Models; +using GitHub.Services; + +namespace GitHub.TeamFoundation.Services +{ + class LocalRepositoryModelFactory : ILocalRepositoryModelFactory + { + public ILocalRepositoryModel Create(string localPath) + { + return new LocalRepositoryModel(localPath, GitService.GitServiceHelper); + } + } +} diff --git a/src/GitHub.TeamFoundation.14/Services/TeamExplorerServices.cs b/src/GitHub.TeamFoundation.14/Services/TeamExplorerServices.cs new file mode 100644 index 0000000000..318ddf3c47 --- /dev/null +++ b/src/GitHub.TeamFoundation.14/Services/TeamExplorerServices.cs @@ -0,0 +1,92 @@ +using System; +using System.ComponentModel.Composition; +using System.Windows.Input; +using GitHub.Extensions; +using GitHub.VisualStudio.TeamExplorer.Sync; +using Microsoft.TeamFoundation.Controls; +using Microsoft.VisualStudio.Shell; + +namespace GitHub.Services +{ + [Export(typeof(ITeamExplorerServices))] + [PartCreationPolicy(CreationPolicy.Shared)] + public class TeamExplorerServices : ITeamExplorerServices + { + readonly IGitHubServiceProvider serviceProvider; + + /// + /// This MEF export requires specific versions of TeamFoundation. ITeamExplorerNotificationManager is declared here so + /// that instances of this type cannot be created if the TeamFoundation dlls are not available + /// (otherwise we'll have multiple instances of ITeamExplorerServices exports, and that would be Bad(tm)) + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")] + ITeamExplorerNotificationManager manager; + + [ImportingConstructor] + public TeamExplorerServices(IGitHubServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + public void ShowConnectPage() + { + var te = serviceProvider.TryGetService(); + var foo = te.NavigateToPage(new Guid(TeamExplorerPageIds.Connect), null); + } + + public void ShowPublishSection() + { + var te = serviceProvider.TryGetService(); + var foo = te.NavigateToPage(new Guid(TeamExplorerPageIds.GitCommits), null); + var publish = foo?.GetSection(new Guid(GitHubPublishSection.GitHubPublishSectionId)) as GitHubPublishSection; + publish?.Connect(); + } + + public void ShowMessage(string message) + { + manager = serviceProvider.GetService(); + manager?.ShowNotification(message, NotificationType.Information, NotificationFlags.None, null, default(Guid)); + } + + public void ShowMessage(string message, ICommand command, bool showToolTips = true, Guid guid = default(Guid)) + { + manager = serviceProvider.GetService(); + manager?.ShowNotification( + message, + NotificationType.Information, + showToolTips ? NotificationFlags.None : NotificationFlags.NoTooltips, + command, + guid); + } + + public void ShowWarning(string message) + { + manager = serviceProvider.GetService(); + manager?.ShowNotification(message, NotificationType.Warning, NotificationFlags.None, null, default(Guid)); + } + + public void ShowError(string message) + { + manager = serviceProvider.GetService(); + manager?.ShowNotification(message, NotificationType.Error, NotificationFlags.None, null, default(Guid)); + } + + public void HideNotification(Guid guid) + { + manager = serviceProvider.GetService(); + manager?.HideNotification(guid); + } + + public void ClearNotifications() + { + manager = serviceProvider.GetService(); + manager?.ClearNotifications(); + } + + public bool IsNotificationVisible(Guid guid) + { + manager = serviceProvider.GetService(); + return manager?.IsNotificationVisible(guid) ?? false; + } + } +} \ No newline at end of file diff --git a/src/GitHub.TeamFoundation.14/Services/VSGitExt.cs b/src/GitHub.TeamFoundation.14/Services/VSGitExt.cs new file mode 100644 index 0000000000..b2279c8b65 --- /dev/null +++ b/src/GitHub.TeamFoundation.14/Services/VSGitExt.cs @@ -0,0 +1,140 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Collections.Generic; +using GitHub.Models; +using GitHub.Services; +using GitHub.Logging; +using GitHub.Extensions; +using GitHub.TeamFoundation.Services; +using Serilog; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Threading; +using Microsoft.VisualStudio.TeamFoundation.Git.Extensibility; +using Task = System.Threading.Tasks.Task; +using static Microsoft.VisualStudio.VSConstants; + +namespace GitHub.VisualStudio.Base +{ + /// + /// This service acts as an always available version of . + /// + /// + /// 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 IAsyncServiceProvider asyncServiceProvider; + readonly ILocalRepositoryModelFactory repositoryFactory; + readonly object refreshLock = new object(); + + IGitExt gitService; + IReadOnlyList activeRepositories; + + public VSGitExt(IAsyncServiceProvider asyncServiceProvider) + : this(asyncServiceProvider, new VSUIContextFactory(), new LocalRepositoryModelFactory(), ThreadHelper.JoinableTaskContext) + { + } + + public VSGitExt(IAsyncServiceProvider asyncServiceProvider, IVSUIContextFactory factory, ILocalRepositoryModelFactory repositoryFactory, + JoinableTaskContext joinableTaskContext) + { + JoinableTaskCollection = joinableTaskContext.CreateCollection(); + JoinableTaskCollection.DisplayName = nameof(VSGitExt); + JoinableTaskFactory = joinableTaskContext.CreateFactory(JoinableTaskCollection); + + this.asyncServiceProvider = asyncServiceProvider; + this.repositoryFactory = repositoryFactory; + + // Start with empty array until we have a chance to initialize. + ActiveRepositories = Array.Empty(); + + // Initialize when we enter the context of a Git repository + var context = factory.GetUIContext(UICONTEXT.RepositoryOpen_guid); + context.WhenActivated(() => JoinableTaskFactory.RunAsync(InitializeAsync).Task.Forget(log)); + } + + async Task InitializeAsync() + { + gitService = await GetServiceAsync(); + if (gitService == null) + { + log.Error("Couldn't find IGitExt service"); + return; + } + + // Refresh on background thread + await Task.Run(() => RefreshActiveRepositories()); + + gitService.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(gitService.ActiveRepositories)) + { + RefreshActiveRepositories(); + } + }; + } + + public void RefreshActiveRepositories() + { + try + { + lock (refreshLock) + { + log.Debug( + "IGitExt.ActiveRepositories (#{Id}) returned {Repositories}", + gitService.GetHashCode(), + gitService.ActiveRepositories.Select(x => x.RepositoryPath)); + + ActiveRepositories = gitService?.ActiveRepositories.Select(x => repositoryFactory.Create(x.RepositoryPath)).ToList(); + } + } + catch (Exception e) + { + log.Error(e, "Error refreshing repositories"); + ActiveRepositories = Array.Empty(); + } + } + + public IReadOnlyList ActiveRepositories + { + get + { + return activeRepositories; + } + + private set + { + if (value != activeRepositories) + { + log.Debug("ActiveRepositories changed to {Repositories}", value?.Select(x => x.CloneUrl)); + activeRepositories = value; + ActiveRepositoriesChanged?.Invoke(); + } + } + } + + public void JoinTillEmpty() + { + JoinableTaskFactory.Context.Factory.Run(async () => + { + await JoinableTaskCollection.JoinTillEmptyAsync(); + }); + } + + async Task GetServiceAsync() + { + await JoinableTaskFactory.SwitchToMainThreadAsync(); + return (T)await asyncServiceProvider.GetServiceAsync(typeof(T)); + } + + + public event Action ActiveRepositoriesChanged; + + JoinableTaskCollection JoinableTaskCollection { get; } + JoinableTaskFactory JoinableTaskFactory { get; } + } +} \ No newline at end of file diff --git a/src/GitHub.TeamFoundation.14/Services/VSGitServices.cs b/src/GitHub.TeamFoundation.14/Services/VSGitServices.cs new file mode 100644 index 0000000000..cb99445424 --- /dev/null +++ b/src/GitHub.TeamFoundation.14/Services/VSGitServices.cs @@ -0,0 +1,151 @@ +#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.Diagnostics.CodeAnalysis; +using System.ComponentModel.Composition; +using System.Globalization; +using System.Linq; +using System.Reactive.Linq; +using System.Threading.Tasks; +using GitHub.Extensions; +using GitHub.Logging; +using GitHub.Models; +using GitHub.TeamFoundation; +using GitHub.VisualStudio; +#if TEAMEXPLORER14 +using Microsoft.TeamFoundation.Git.Controls.Extensibility; +using ReactiveUI; +#else +using Microsoft.VisualStudio.Shell.Interop; +using System.Threading; +#endif +using Microsoft.VisualStudio.TeamFoundation.Git.Extensibility; +using Serilog; + +namespace GitHub.Services +{ + [Export(typeof(IVSGitServices))] + [PartCreationPolicy(CreationPolicy.Shared)] + public class VSGitServices : IVSGitServices + { + static readonly ILogger log = LogManager.ForContext(); + + readonly IGitHubServiceProvider serviceProvider; + + [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Justification = "Used in VS2017")] + readonly Lazy statusBar; + + /// + /// This MEF export requires specific versions of TeamFoundation. IGitExt is declared here so + /// that instances of this type cannot be created if the TeamFoundation dlls are not available + /// (otherwise we'll have multiple instances of IVSServices exports, and that would be Bad(tm)) + /// + [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")] + IGitExt gitExtService; + + [ImportingConstructor] + public VSGitServices(IGitHubServiceProvider serviceProvider, Lazy statusBar) + { + this.serviceProvider = serviceProvider; + this.statusBar = statusBar; + } + + // The Default Repository Path that VS uses is hidden in an internal + // service 'ISccSettingsService' registered in an internal service + // 'ISccServiceHost' in an assembly with no public types that's + // always loaded with VS if the git service provider is loaded + public string GetLocalClonePathFromGitProvider() + { + string ret = string.Empty; + + try + { + ret = RegistryHelper.PokeTheRegistryForLocalClonePath(); + } + catch (Exception ex) + { + log.Error(ex, "Error loading the default cloning path from the registry"); + } + return ret; + } + + /// + public async Task Clone( + string cloneUrl, + string clonePath, + bool recurseSubmodules, + object progress = null) + { +#if TEAMEXPLORER14 + var gitExt = serviceProvider.GetService(); + gitExt.Clone(cloneUrl, clonePath, recurseSubmodules ? CloneOptions.RecurseSubmodule : CloneOptions.None); + + // The operation will have completed when CanClone goes false and then true again. + await gitExt.WhenAnyValue(x => x.CanClone).Where(x => !x).Take(1); + await gitExt.WhenAnyValue(x => x.CanClone).Where(x => x).Take(1); +#else + var gitExt = serviceProvider.GetService(); + var typedProgress = ((Progress)progress) ?? new Progress(); + + await Microsoft.VisualStudio.Shell.ThreadHelper.JoinableTaskFactory.RunAsync(async () => + { + typedProgress.ProgressChanged += (s, e) => statusBar.Value.ShowMessage(e.ProgressText); + await gitExt.CloneAsync(cloneUrl, clonePath, recurseSubmodules, default(CancellationToken), typedProgress); + }); +#endif + } + + IGitRepositoryInfo GetRepoFromVS() + { + gitExtService = serviceProvider.GetService(); + return gitExtService.ActiveRepositories.FirstOrDefault(); + } + + public LibGit2Sharp.IRepository GetActiveRepo() + { + var repo = GetRepoFromVS(); + return repo != null + ? serviceProvider.GetService().GetRepository(repo.RepositoryPath) + : serviceProvider.GetSolution().GetRepositoryFromSolution(); + } + + public string GetActiveRepoPath() + { + string ret = null; + var repo = GetRepoFromVS(); + if (repo != null) + ret = repo.RepositoryPath; + if (ret == null) + { + using (var repository = serviceProvider.GetSolution().GetRepositoryFromSolution()) + { + ret = repository?.Info?.Path; + } + } + return ret ?? String.Empty; + } + + public IEnumerable GetKnownRepositories() + { + try + { + return RegistryHelper.PokeTheRegistryForRepositoryList(); + } + catch (Exception ex) + { + log.Error(ex, "Error loading the repository list from the registry"); + return Enumerable.Empty(); + } + } + + public string SetDefaultProjectPath(string path) + { + return RegistryHelper.SetDefaultProjectPath(path); + } + } +} diff --git a/src/GitHub.TeamFoundation.14/Services/VSUIContextFactory.cs b/src/GitHub.TeamFoundation.14/Services/VSUIContextFactory.cs new file mode 100644 index 0000000000..3797f75c02 --- /dev/null +++ b/src/GitHub.TeamFoundation.14/Services/VSUIContextFactory.cs @@ -0,0 +1,28 @@ +using System; +using Microsoft.VisualStudio.Shell; +using GitHub.Services; + +namespace GitHub.TeamFoundation.Services +{ + class VSUIContextFactory : IVSUIContextFactory + { + public IVSUIContext GetUIContext(Guid contextGuid) + { + return new VSUIContext(UIContext.FromUIContextGuid(contextGuid)); + } + } + + class VSUIContext : IVSUIContext + { + readonly UIContext context; + + public VSUIContext(UIContext context) + { + this.context = context; + } + + public bool IsActive { get { return context.IsActive; } } + + public void WhenActivated(Action action) => context.WhenActivated(action); + } +} diff --git a/src/GitHub.TeamFoundation.14/Settings.cs b/src/GitHub.TeamFoundation.14/Settings.cs new file mode 100644 index 0000000000..ce32331239 --- /dev/null +++ b/src/GitHub.TeamFoundation.14/Settings.cs @@ -0,0 +1,18 @@ +// Guids.cs +// MUST match guids.h +using Microsoft.TeamFoundation.Controls; +using System; + +namespace GitHub.VisualStudio +{ + + static class NavigationItemPriority + { + public const int PullRequests = TeamExplorerNavigationItemPriority.GitCommits - 1; + public const int Wiki = TeamExplorerNavigationItemPriority.Settings - 2; + public const int Fork = TeamExplorerNavigationItemPriority.Settings - 1; + public const int Pulse = Wiki - 3; + public const int Graphs = Wiki - 2; + public const int Issues = Wiki - 1; + } +} diff --git a/src/GitHub.TeamFoundation.14/Sync/EnsureLoggedInSectionSync.cs b/src/GitHub.TeamFoundation.14/Sync/EnsureLoggedInSectionSync.cs new file mode 100644 index 0000000000..7bafa574f5 --- /dev/null +++ b/src/GitHub.TeamFoundation.14/Sync/EnsureLoggedInSectionSync.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.Composition; +using GitHub.Api; +using GitHub.Services; + +namespace GitHub.VisualStudio.TeamExplorer.Sync +{ + // TODO: The IsAGitHubRepo() is somehow giving false positives, need to fix that + // before reactivating this, it's annoying users. + //[TeamExplorerSection(SyncLoginSectionId, TeamExplorerPageIds.GitCommits, 10)] + //[PartCreationPolicy(CreationPolicy.NonShared)] + public class EnsureLoggedInSectionSync : EnsureLoggedInSection + { + public const string SyncLoginSectionId = "C5975729-3CF1-47B4-AE92-C2934906CDDA"; + + [ImportingConstructor] + public EnsureLoggedInSectionSync(IGitHubServiceProvider serviceProvider, + ISimpleApiClientFactory apiFactory, ITeamExplorerServiceHolder holder, + IConnectionManager cm, ITeamExplorerServices teServices, + IDialogService dialogService) + : base(serviceProvider, apiFactory, holder, cm, teServices, dialogService) + {} + } +} \ No newline at end of file diff --git a/src/GitHub.TeamFoundation.14/Sync/GitHubPublishSection.cs b/src/GitHub.TeamFoundation.14/Sync/GitHubPublishSection.cs new file mode 100644 index 0000000000..3ce2d7c286 --- /dev/null +++ b/src/GitHub.TeamFoundation.14/Sync/GitHubPublishSection.cs @@ -0,0 +1,240 @@ +using System; +using System.ComponentModel.Composition; +using System.Globalization; +using System.Reactive.Linq; +using System.Threading.Tasks; +using System.Windows; +using GitHub.Api; +using GitHub.Extensions; +using GitHub.Factories; +using GitHub.Info; +using GitHub.Models; +using GitHub.Primitives; +using GitHub.Services; +using GitHub.UI; +using GitHub.ViewModels; +using GitHub.ViewModels.TeamExplorer; +using GitHub.VisualStudio.Base; +using GitHub.VisualStudio.Helpers; +using GitHub.VisualStudio.UI; +using GitHub.VisualStudio.UI.Views; +using Microsoft.TeamFoundation.Controls; +using Microsoft.VisualStudio; +using ReactiveUI; + +namespace GitHub.VisualStudio.TeamExplorer.Sync +{ + [TeamExplorerSection(GitHubPublishSectionId, TeamExplorerPageIds.GitCommits, 10)] + [PartCreationPolicy(CreationPolicy.NonShared)] + public class GitHubPublishSection : TeamExplorerSectionBase, IGitHubInvitationSection + { + public const string GitHubPublishSectionId = "92655B52-360D-4BF5-95C5-D9E9E596AC76"; + + readonly Lazy lazyBrowser; + readonly IDialogService dialogService; + bool loggedIn; + + [ImportingConstructor] + public GitHubPublishSection(IGitHubServiceProvider serviceProvider, + ISimpleApiClientFactory apiFactory, ITeamExplorerServiceHolder holder, + IConnectionManager cm, Lazy browser, + IDialogService dialogService) + : base(serviceProvider, apiFactory, holder, cm) + { + + lazyBrowser = browser; + this.dialogService = dialogService; + Title = Resources.GitHubPublishSectionTitle; + Name = "GitHub"; + Provider = "GitHub, Inc"; + Description = Resources.BlurbText; + ShowLogin = false; + ShowSignup = false; + ShowGetStarted = false; + IsVisible = false; + IsExpanded = true; + InitializeSectionView(); + } + + void InitializeSectionView() + { + var view = new GitHubInvitationContent(); + SectionContent = view; + view.DataContext = this; + } + + async void Setup() + { + if (ActiveRepo != null && ActiveRepoUri == null) + { + IsVisible = true; + ShowGetStarted = true; + loggedIn = await connectionManager.IsLoggedIn(); + ShowLogin = !loggedIn; + ShowSignup = !loggedIn; + } + else + IsVisible = false; + } + + public override void Initialize(IServiceProvider serviceProvider) + { + base.Initialize(serviceProvider); + Setup(); + } + + protected override void RepoChanged(bool changed) + { + base.RepoChanged(changed); + Setup(); + InitializeSectionView(); + } + + public async Task Connect() + { + loggedIn = await connectionManager.IsLoggedIn(); + if (loggedIn) + ShowPublish(); + else + await Login(); + } + + public void SignUp() + { + OpenInBrowser(lazyBrowser, GitHubUrls.Plans); + } + + public void ShowPublish() + { + var factory = ServiceProvider.GetService(); + var viewModel = ServiceProvider.GetService(); + var busy = viewModel.WhenAnyValue(x => x.IsBusy).Subscribe(x => IsBusy = x); + var completed = viewModel.PublishRepository + .Where(x => x == ProgressState.Success) + .Subscribe(_ => + { + ServiceProvider.TryGetService()?.NavigateToPage(new Guid(TeamExplorerPageIds.Home), null); + HandleCreatedRepo(ActiveRepo); + }); + + var view = factory.CreateView(); + view.DataContext = viewModel; + SectionContent = view; + + Observable.FromEventPattern( + x => view.Unloaded += x, + x => view.Unloaded -= x) + .Take(1) + .Subscribe(_ => + { + busy.Dispose(); + completed.Dispose(); + }); + } + + void HandleCreatedRepo(ILocalRepositoryModel newrepo) + { + var msg = String.Format(CultureInfo.CurrentCulture, Constants.Notification_RepoCreated, newrepo.Name, newrepo.CloneUrl); + msg += " " + String.Format(CultureInfo.CurrentCulture, Constants.Notification_CreateNewProject, newrepo.LocalPath); + ShowNotification(newrepo, msg); + } + + private void ShowNotification(ILocalRepositoryModel newrepo, string msg) + { + var teServices = ServiceProvider.TryGetService(); + + teServices.ClearNotifications(); + teServices.ShowMessage( + msg, + new RelayCommand(o => + { + var str = o.ToString(); + /* the prefix is the action to perform: + * u: launch browser with url + * c: launch create new project dialog + * o: launch open existing project dialog + */ + var prefix = str.Substring(0, 2); + if (prefix == "u:") + OpenInBrowser(ServiceProvider.TryGetService(), new Uri(str.Substring(2))); + else if (prefix == "o:") + { + if (ErrorHandler.Succeeded(ServiceProvider.GetSolution().OpenSolutionViaDlg(str.Substring(2), 1))) + ServiceProvider.TryGetService()?.NavigateToPage(new Guid(TeamExplorerPageIds.Home), null); + } + else if (prefix == "c:") + { + var vsGitServices = ServiceProvider.TryGetService(); + vsGitServices.SetDefaultProjectPath(newrepo.LocalPath); + if (ErrorHandler.Succeeded(ServiceProvider.GetSolution().CreateNewProjectViaDlg(null, null, 0))) + ServiceProvider.TryGetService()?.NavigateToPage(new Guid(TeamExplorerPageIds.Home), null); + } + }) + ); + } + + async Task Login() + { + await dialogService.ShowLoginDialog(); + loggedIn = await connectionManager.IsLoggedIn(); + if (loggedIn) + ShowPublish(); + } + + bool disposed; + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (!disposed) + { + disposed = true; + } + } + base.Dispose(disposing); + } + + string name = String.Empty; + public string Name + { + get { return name; } + set { name = value; this.RaisePropertyChange(); } + } + + string provider = String.Empty; + public string Provider + { + get { return provider; } + set { provider = value; this.RaisePropertyChange(); } + } + + string description = String.Empty; + public string Description + { + get { return description; } + set { description = value; this.RaisePropertyChange(); } + } + + bool showLogin; + public bool ShowLogin + { + get { return showLogin; } + set { showLogin = value; this.RaisePropertyChange(); } + } + + + bool showSignup; + public bool ShowSignup + { + get { return showSignup; } + set { showSignup = value; this.RaisePropertyChange(); } + } + + bool showGetStarted; + public bool ShowGetStarted + { + get { return showGetStarted; } + set { showGetStarted = value; this.RaisePropertyChange(); } + } + } +} diff --git a/src/GitHub.TeamFoundation.14/packages.config b/src/GitHub.TeamFoundation.14/packages.config new file mode 100644 index 0000000000..a87a881aa1 --- /dev/null +++ b/src/GitHub.TeamFoundation.14/packages.config @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.TeamFoundation.15/GitHub.TeamFoundation.15.csproj b/src/GitHub.TeamFoundation.15/GitHub.TeamFoundation.15.csproj new file mode 100644 index 0000000000..9306a1e81b --- /dev/null +++ b/src/GitHub.TeamFoundation.15/GitHub.TeamFoundation.15.csproj @@ -0,0 +1,321 @@ + + + + + + $(MSBuildToolsVersion) + $(MSBuildToolsVersion) + Debug + AnyCPU + {161DBF01-1DBF-4B00-8551-C5C00F26720E} + Library + Properties + GitHub.TeamFoundation + GitHub.TeamFoundation.15 + 7.3 + v4.6.1 + 512 + ..\common\GitHubVS.ruleset + true + true + + + + + true + full + false + DEBUG;TRACE;TEAMEXPLORER15 + prompt + 4 + false + bin\Debug\ + + + true + full + false + CODE_ANALYSIS;DEBUG;TRACE;TEAMEXPLORER15 + prompt + 4 + true + bin\Debug\ + + + pdbonly + true + TRACE;TEAMEXPLORER15 + prompt + 4 + true + bin\Release\ + + + + + ..\..\packages\LibGit2Sharp.0.23.1\lib\net40\LibGit2Sharp.dll + True + + + ..\..\lib\15.0\Microsoft.TeamFoundation.Common.dll + False + + + ..\..\lib\15.0\Microsoft.TeamFoundation.Client.dll + False + + + ..\..\lib\15.0\Microsoft.TeamFoundation.Controls.dll + False + + + ..\..\lib\15.0\Microsoft.TeamFoundation.Git.Controls.dll + False + + + ..\..\lib\15.0\Microsoft.TeamFoundation.Git.Provider.dll + False + + + ..\..\packages\Microsoft.VisualStudio.CoreUtility.15.4.27004\lib\net45\Microsoft.VisualStudio.CoreUtility.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Imaging.15.4.27004\lib\net45\Microsoft.VisualStudio.Imaging.dll + True + + + ..\..\packages\Microsoft.VisualStudio.OLE.Interop.7.10.6070\lib\Microsoft.VisualStudio.OLE.Interop.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Shell.15.0.15.0.26228\lib\Microsoft.VisualStudio.Shell.15.0.dll + + + ..\..\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 + True + + + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.15.3.DesignTime.15.0.26606\lib\net20\Microsoft.VisualStudio.Shell.Interop.15.3.DesignTime.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.8.0.8.0.50727\lib\Microsoft.VisualStudio.Shell.Interop.8.0.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Shell.Interop.9.0.9.0.30729\lib\Microsoft.VisualStudio.Shell.Interop.9.0.dll + True + + + ..\..\packages\Microsoft.VisualStudio.TextManager.Interop.7.10.6070\lib\Microsoft.VisualStudio.TextManager.Interop.dll + True + + + ..\..\packages\Microsoft.VisualStudio.TextManager.Interop.8.0.8.0.50727\lib\Microsoft.VisualStudio.TextManager.Interop.8.0.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Threading.15.0.240\lib\net45\Microsoft.VisualStudio.Threading.dll + + + ..\..\packages\Microsoft.VisualStudio.Utilities.15.4.27004\lib\net46\Microsoft.VisualStudio.Utilities.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Validation.15.3.15\lib\net45\Microsoft.VisualStudio.Validation.dll + True + + + + + ..\..\packages\Serilog.2.5.0\lib\net46\Serilog.dll + True + + + ..\..\packages\Stateless.2.5.56.0\lib\portable-net40+sl50+win+wp80+MonoAndroid10+xamarinios10+MonoTouch10\Stateless.dll + True + + + + + + + ..\..\packages\Rx-Core.2.2.5-custom\lib\net45\System.Reactive.Core.dll + True + + + ..\..\packages\Rx-Interfaces.2.2.5-custom\lib\net45\System.Reactive.Interfaces.dll + True + + + False + ..\..\packages\Rx-Linq.2.2.5-custom\lib\net45\System.Reactive.Linq.dll + + + + + + + + + + + + + Home\ForkNavigationItem.cs + + + Services\LocalRepositoryModelFactory.cs + + + Services\VSGitExt.cs + + + Services\VSUIContextFactory.cs + + + Settings.cs + + + Base\EnsureLoggedInSection.cs + + + Base\TeamExplorerInvitationBase.cs + + + Base\TeamExplorerNavigationItemBase.cs + + + Base\TeamExplorerSectionBase.cs + + + Base\TeamExplorerServiceHolder.cs + + + Connect\GitHubConnectSection.cs + + + Connect\GitHubConnectSection0.cs + + + Connect\GitHubConnectSection1.cs + + + Connect\GitHubInvitationSection.cs + + + Home\GitHubHomeSection.cs + + + Home\GraphsNavigationItem.cs + + + Home\IssuesNavigationItem.cs + + + Home\PullRequestsNavigationItem.cs + + + Home\PulseNavigationItem.cs + + + Home\WikiNavigationItem.cs + + + Sync\EnsureLoggedInSectionSync.cs + + + Sync\GitHubPublishSection.cs + + + Services\TeamExplorerServices.cs + + + Services\VSGitServices.cs + + + + Properties\SolutionInfo.cs + + + + Designer + + + + + {08dd4305-7787-4823-a53f-4d0f725a07f3} + Octokit + + + {1ce2d235-8072-4649-ba5a-cfb1af8776e0} + ReactiveUI_Net45 + + + {252ce1c2-027a-4445-a3c2-e4d6c80a935a} + Splat-Net45 + + + {b389adaf-62cc-486e-85b4-2d8b078df763} + GitHub.Api + + + {1A1DA411-8D1F-4578-80A6-04576BEA2DC5} + GitHub.App + + + {E4ED0537-D1D9-44B6-9212-3096D7C3F7A1} + GitHub.Exports.Reactive + + + {9aea02db-02b5-409c-b0ca-115d05331a6b} + GitHub.Exports + + + {6afe2e2d-6db0-4430-a2ea-f5f5388d2f78} + GitHub.Extensions + + + {8d73575a-a89f-47cc-b153-b47dd06837f0} + GitHub.Logging + + + {d1dfbb0c-b570-4302-8f1e-2e3a19c41961} + GitHub.VisualStudio.UI + + + + + + + + + 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}. + + + + + + + \ No newline at end of file diff --git a/src/GitHub.TeamFoundation.15/Properties/AssemblyInfo.cs b/src/GitHub.TeamFoundation.15/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..6dc9797bbf --- /dev/null +++ b/src/GitHub.TeamFoundation.15/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("GitHub.TeamFoundation.14")] +[assembly: AssemblyDescription("GitHub TeamFoundation")] +[assembly: Guid("b389adaf-62cc-486e-85b4-2d8b078df763")] diff --git a/src/GitHub.TeamFoundation.15/RegistryHelper.cs b/src/GitHub.TeamFoundation.15/RegistryHelper.cs new file mode 100644 index 0000000000..54ca566bb1 --- /dev/null +++ b/src/GitHub.TeamFoundation.15/RegistryHelper.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using GitHub.Logging; +using GitHub.Models; +using GitHub.Services; +using Microsoft.Win32; +using Serilog; + +namespace GitHub.TeamFoundation +{ + internal class RegistryHelper + { + static readonly ILogger log = LogManager.ForContext(); + const string TEGitKey = @"Software\Microsoft\VisualStudio\15.0\TeamFoundation\GitSourceControl"; + static RegistryKey OpenGitKey(string path) + { + return Microsoft.Win32.Registry.CurrentUser.OpenSubKey(TEGitKey + "\\" + path, true); + } + + internal static IEnumerable PokeTheRegistryForRepositoryList() + { + using (var key = OpenGitKey("Repositories")) + { + return key.GetSubKeyNames().Select(x => + { + using (var subkey = key.OpenSubKey(x)) + { + try + { + var path = subkey?.GetValue("Path") as string; + if (path != null) + return new LocalRepositoryModel(path, GitService.GitServiceHelper); + } + catch (Exception) + { + // no sense spamming the log, the registry might have ton of stale things we don't care about + } + return null; + } + }) + .Where(x => x != null) + .ToList(); + } + } + + internal static string PokeTheRegistryForLocalClonePath() + { + using (var key = OpenGitKey("General")) + { + return (string)key?.GetValue("DefaultRepositoryPath", string.Empty, RegistryValueOptions.DoNotExpandEnvironmentNames); + } + } + + const string NewProjectDialogKeyPath = @"Software\Microsoft\VisualStudio\15.0\NewProjectDialog"; + const string MRUKeyPath = "MRUSettingsLocalProjectLocationEntries"; + internal static string SetDefaultProjectPath(string path) + { + var old = String.Empty; + try + { + var newProjectKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(NewProjectDialogKeyPath, true) ?? + Microsoft.Win32.Registry.CurrentUser.CreateSubKey(NewProjectDialogKeyPath); + + if (newProjectKey == null) + { + throw new GitHubLogicException( + string.Format( + CultureInfo.CurrentCulture, + "Could not open or create registry key '{0}'", + NewProjectDialogKeyPath)); + } + + using (newProjectKey) + { + var mruKey = newProjectKey.OpenSubKey(MRUKeyPath, true) ?? + Microsoft.Win32.Registry.CurrentUser.CreateSubKey(MRUKeyPath); + + if (mruKey == null) + { + throw new GitHubLogicException( + string.Format( + CultureInfo.CurrentCulture, + "Could not open or create registry key '{0}'", + MRUKeyPath)); + } + + using (mruKey) + { + // is this already the default path? bail + old = (string)mruKey.GetValue("Value0", string.Empty, RegistryValueOptions.DoNotExpandEnvironmentNames); + if (String.Equals(path.TrimEnd('\\'), old.TrimEnd('\\'), StringComparison.CurrentCultureIgnoreCase)) + return old; + + // grab the existing list of recent paths, throwing away the last one + var numEntries = (int)mruKey.GetValue("MaximumEntries", 5); + var entries = new List(numEntries); + for (int i = 0; i < numEntries - 1; i++) + { + var val = (string)mruKey.GetValue("Value" + i, String.Empty, RegistryValueOptions.DoNotExpandEnvironmentNames); + if (!String.IsNullOrEmpty(val)) + entries.Add(val); + } + + newProjectKey.SetValue("LastUsedNewProjectPath", path); + mruKey.SetValue("Value0", path); + // bump list of recent paths one entry down + for (int i = 0; i < entries.Count; i++) + mruKey.SetValue("Value" + (i + 1), entries[i]); + } + } + } + catch (Exception ex) + { + log.Error(ex, "Error setting the create project path in the registry"); + } + return old; + } + } +} diff --git a/src/GitHub.TeamFoundation.15/packages.config b/src/GitHub.TeamFoundation.15/packages.config new file mode 100644 index 0000000000..e6dc9b355a --- /dev/null +++ b/src/GitHub.TeamFoundation.15/packages.config @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.UI.Reactive/Assets/Controls.xaml b/src/GitHub.UI.Reactive/Assets/Controls.xaml index 63c2c19457..a5fdd7125d 100644 --- a/src/GitHub.UI.Reactive/Assets/Controls.xaml +++ b/src/GitHub.UI.Reactive/Assets/Controls.xaml @@ -12,5 +12,4 @@ - diff --git a/src/GitHub.UI.Reactive/Assets/Controls/ErrorMessageDisplay.xaml b/src/GitHub.UI.Reactive/Assets/Controls/ErrorMessageDisplay.xaml index f7292b143b..eea3a757fc 100644 --- a/src/GitHub.UI.Reactive/Assets/Controls/ErrorMessageDisplay.xaml +++ b/src/GitHub.UI.Reactive/Assets/Controls/ErrorMessageDisplay.xaml @@ -1,6 +1,6 @@  - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/src/GitHub.UI.Reactive/Controls/TwoFactorInput.xaml.cs b/src/GitHub.UI.Reactive/Controls/TwoFactorInput.xaml.cs index 733c9513f8..099322afdf 100644 --- a/src/GitHub.UI.Reactive/Controls/TwoFactorInput.xaml.cs +++ b/src/GitHub.UI.Reactive/Controls/TwoFactorInput.xaml.cs @@ -4,15 +4,14 @@ using System.Windows.Controls; using System.Windows.Input; using GitHub.Extensions; -using NullGuard; using System.Globalization; namespace GitHub.UI { public class TwoFactorInputToTextBox : ValueConverterMarkupExtension { - public override object Convert([AllowNull] object value, [AllowNull] Type targetType, - [AllowNull] object parameter, [AllowNull] CultureInfo culture) + public override object Convert(object value, Type targetType, + object parameter, CultureInfo culture) { return value is TwoFactorInput ? ((TwoFactorInput)value).TextBox : null; } @@ -56,6 +55,9 @@ public IObservable TryFocus() private void OnPaste(object sender, DataObjectPastingEventArgs e) { + Guard.ArgumentNotNull(sender, nameof(sender)); + Guard.ArgumentNotNull(e, nameof(e)); + var isText = e.SourceDataObject.GetDataPresent(DataFormats.Text, true); if (!isText) return; @@ -84,16 +86,16 @@ void SetText(string text) SetValue(TextProperty, String.Join("", digits)); } - [AllowNull] public string Text { - [return: AllowNull] get { return (string)GetValue(TextProperty); } set { SetText(value); } } private void SetupTextBox(TextBox textBox) { + Guard.ArgumentNotNull(textBox, nameof(textBox)); + DataObject.AddPastingHandler(textBox, new DataObjectPastingEventHandler(OnPaste)); textBox.GotFocus += (sender, args) => textBox.SelectAll(); @@ -182,6 +184,8 @@ bool MoveFocus(FocusNavigationDirection navigationDirection) private static string GetTextBoxValue(TextBox textBox) { + Guard.ArgumentNotNull(textBox, nameof(textBox)); + return String.IsNullOrEmpty(textBox.Text) ? " " : textBox.Text; } diff --git a/src/GitHub.UI.Reactive/Controls/Validation/UserErrorMessages.cs b/src/GitHub.UI.Reactive/Controls/Validation/UserErrorMessages.cs index 39c73ae3fb..5dc77f7873 100644 --- a/src/GitHub.UI.Reactive/Controls/Validation/UserErrorMessages.cs +++ b/src/GitHub.UI.Reactive/Controls/Validation/UserErrorMessages.cs @@ -7,7 +7,6 @@ using System.Windows.Media; using GitHub.Extensions; using GitHub.Extensions.Reactive; -using NullGuard; using ReactiveUI; namespace GitHub.UI @@ -32,24 +31,11 @@ public UserErrorMessages() { DataContext = result; }); - - Unloaded += (o, e) => - { - if (whenAnyShowingMessage != null) - { - whenAnyShowingMessage.Dispose(); - } - if (whenAnyDataContext != null) - { - whenAnyDataContext.Dispose(); - } - }; } - public static readonly DependencyProperty IconMarginProperty = DependencyProperty.Register("IconMargin", typeof(Thickness), typeof(UserErrorMessages), new PropertyMetadata(new Thickness(0,10,7,0))); + public static readonly DependencyProperty IconMarginProperty = DependencyProperty.Register("IconMargin", typeof(Thickness), typeof(UserErrorMessages), new PropertyMetadata(new Thickness(0,0,8,0))); public Thickness IconMargin { - [return: AllowNull] get { return (Thickness)GetValue(IconMarginProperty); } set { SetValue(IconMarginProperty, value); } } @@ -57,7 +43,6 @@ public Thickness IconMargin public static readonly DependencyProperty MessageMarginProperty = DependencyProperty.Register("MessageMargin", typeof(Thickness), typeof(UserErrorMessages)); public Thickness MessageMargin { - [return: AllowNull] get { return (Thickness)GetValue(MessageMarginProperty); } set { SetValue(MessageMarginProperty, value); } } @@ -65,7 +50,6 @@ public Thickness MessageMargin public static readonly DependencyProperty IconProperty = DependencyProperty.Register("Icon", typeof(Octicon), typeof(UserErrorMessages), new PropertyMetadata(Octicon.stop)); public Octicon Icon { - [return: AllowNull] get { return (Octicon)GetValue(IconProperty); } set { SetValue(IconProperty, value); } } @@ -73,7 +57,6 @@ public Octicon Icon public static readonly DependencyProperty FillProperty = DependencyProperty.Register("Fill", typeof(Brush), typeof(UserErrorMessages), new PropertyMetadata(new SolidColorBrush(Color.FromRgb(0xe7, 0x4c, 0x3c)))); public Brush Fill { - [return: AllowNull] get { return (Brush)GetValue(FillProperty); } set { SetValue(FillProperty, value); } } @@ -81,7 +64,6 @@ public Brush Fill public static readonly DependencyProperty ErrorMessageFontWeightProperty = DependencyProperty.Register("ErrorMessageFontWeight", typeof(FontWeight), typeof(UserErrorMessages), new PropertyMetadata(FontWeights.Normal)); public FontWeight ErrorMessageFontWeight { - [return: AllowNull] get { return (FontWeight)GetValue(ErrorMessageFontWeightProperty); } set { SetValue(ErrorMessageFontWeightProperty, value); } } @@ -94,10 +76,8 @@ public bool IsShowingMessage } public static readonly DependencyProperty UserErrorProperty = DependencyProperty.Register("UserError", typeof(UserError), typeof(UserErrorMessages)); - [AllowNull] public UserError UserError { - [return: AllowNull] get { return (UserError)GetValue(UserErrorProperty); } set { SetValue(UserErrorProperty, value); } } @@ -106,18 +86,14 @@ public UserError UserError Justification = "We're registering a handler for a type so this is appropriate.")] public IDisposable RegisterHandler(IObservable clearWhen) where TUserError : UserError { - if (IsVisible) + return UserError.RegisterHandler(userError => { - return UserError.RegisterHandler(userError => - { - UserError = userError; - return clearWhen - .Skip(1) - .Do(_ => UserError = null) - .Select(x => RecoveryOptionResult.CancelOperation); - }); - } - return Disposable.Empty; + UserError = userError; + return clearWhen + .Skip(1) + .Do(_ => UserError = null) + .Select(x => RecoveryOptionResult.CancelOperation); + }); } } } diff --git a/src/GitHub.UI.Reactive/Controls/Validation/ValidationMessage.cs b/src/GitHub.UI.Reactive/Controls/Validation/ValidationMessage.cs index 0fe6eb9063..de1a49b7af 100644 --- a/src/GitHub.UI.Reactive/Controls/Validation/ValidationMessage.cs +++ b/src/GitHub.UI.Reactive/Controls/Validation/ValidationMessage.cs @@ -9,7 +9,6 @@ using System.Windows.Media; using GitHub.Extensions.Reactive; using GitHub.Validation; -using NullGuard; using ReactiveUI; namespace GitHub.UI @@ -17,6 +16,7 @@ namespace GitHub.UI public class ValidationMessage : UserControl { const double defaultTextChangeThrottle = 0.2; + bool userHasInteracted; public ValidationMessage() { @@ -33,6 +33,7 @@ public ValidationMessage() .Do(CreateBinding) .Select(control => Observable.Merge( + this.WhenAnyValue(x => x.ShowError).Where(x => userHasInteracted), control.Events().TextChanged .Throttle(TimeSpan.FromSeconds(ShowError ? defaultTextChangeThrottle : TextChangeThrottle), RxApp.MainThreadScheduler) @@ -56,7 +57,6 @@ public bool IsShowingMessage public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(ValidationMessage)); public string Text { - [return: AllowNull] get { return (string)GetValue(TextProperty); } private set { SetValue(TextProperty, value); } } @@ -78,7 +78,6 @@ public double TextChangeThrottle public static readonly DependencyProperty ValidatesControlProperty = DependencyProperty.Register("ValidatesControl", typeof(TextBox), typeof(ValidationMessage), new PropertyMetadata(default(TextBox))); public TextBox ValidatesControl { - [return: AllowNull] get { return (TextBox)GetValue(ValidatesControlProperty); } set { SetValue(ValidatesControlProperty, value); } } @@ -86,7 +85,6 @@ public TextBox ValidatesControl public static readonly DependencyProperty ReactiveValidatorProperty = DependencyProperty.Register("ReactiveValidator", typeof(ReactivePropertyValidator), typeof(ValidationMessage)); public ReactivePropertyValidator ReactiveValidator { - [return: AllowNull] get { return (ReactivePropertyValidator)GetValue(ReactiveValidatorProperty); } set { SetValue(ReactiveValidatorProperty, value); } } @@ -94,7 +92,6 @@ public ReactivePropertyValidator ReactiveValidator public static readonly DependencyProperty IconProperty = DependencyProperty.Register("Icon", typeof(Octicon), typeof(ValidationMessage), new PropertyMetadata(Octicon.stop)); public Octicon Icon { - [return: AllowNull] get { return (Octicon) GetValue(IconProperty); } set { SetValue(IconProperty, value); } } @@ -103,16 +100,13 @@ public Octicon Icon DependencyProperty.Register("Fill", typeof(Brush), typeof(ValidationMessage), new PropertyMetadata(new SolidColorBrush(Color.FromRgb(0xe7, 0x4c, 0x3c)))); public Brush Fill { - [return: AllowNull] get { return (Brush)GetValue(FillProperty); } set { SetValue(FillProperty, value); } } public static readonly DependencyProperty ErrorAdornerTemplateProperty = DependencyProperty.Register("ErrorAdornerTemplate", typeof(string), typeof(ValidationMessage), new PropertyMetadata("validationTemplate")); - [AllowNull] public string ErrorAdornerTemplate { - [return: AllowNull] get { return (string)GetValue(ErrorAdornerTemplateProperty); } set { SetValue(ErrorAdornerTemplateProperty, value); } } @@ -120,6 +114,7 @@ public string ErrorAdornerTemplate void ShowValidateError(bool showError) { IsShowingMessage = showError; + userHasInteracted = true; if (ValidatesControl == null || !IsAdornerEnabled()) return; diff --git a/src/GitHub.UI.Reactive/Controls/ViewBase.cs b/src/GitHub.UI.Reactive/Controls/ViewBase.cs new file mode 100644 index 0000000000..38a95271b9 --- /dev/null +++ b/src/GitHub.UI.Reactive/Controls/ViewBase.cs @@ -0,0 +1,68 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Automation.Peers; +using GitHub.ViewModels; +using ReactiveUI; +using System.Reactive.Linq; + +namespace GitHub.UI +{ + /// + /// Base class for views. + /// + public class ViewBase : UserControl, IViewFor + where TInterface : class, IViewModel + where TImplementor : class + { + public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register( + "ViewModel", typeof(TInterface), typeof(TImplementor), new PropertyMetadata(null)); + + /// + /// Initializes a new instance of the class. + /// + public ViewBase() + { + DataContextChanged += (s, e) => ViewModel = (TInterface)e.NewValue; + this.WhenAnyValue(x => x.ViewModel).Skip(1).Subscribe(x => DataContext = x); + } + + /// + /// Gets or sets the control's data context as a typed view model. + /// + public TInterface ViewModel + { + get { return (TInterface)GetValue(ViewModelProperty); } + set { SetValue(ViewModelProperty, value); } + } + + /// + /// Gets or sets the control's data context as a typed view model. Required for interaction + /// with ReactiveUI. + /// + TInterface IViewFor.ViewModel + { + get { return ViewModel; } + set { ViewModel = value; } + } + + /// + /// Gets or sets the control's data context. Required for interaction with ReactiveUI. + /// + object IViewFor.ViewModel + { + get { return ViewModel; } + set { ViewModel = (TInterface)value; } + } + + /// + /// Add an automation peer to views and custom controls + /// They do not have automation peers or properties by default + /// https://stackoverflow.com/questions/30198109/automationproperties-automationid-on-custom-control-not-exposed + /// + protected override AutomationPeer OnCreateAutomationPeer() + { + return new UIElementAutomationPeer(this); + } + } +} diff --git a/src/GitHub.UI.Reactive/FodyWeavers.xml b/src/GitHub.UI.Reactive/FodyWeavers.xml deleted file mode 100644 index 9321cb912f..0000000000 --- a/src/GitHub.UI.Reactive/FodyWeavers.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/GitHub.UI.Reactive/GitHub.UI.Reactive.csproj b/src/GitHub.UI.Reactive/GitHub.UI.Reactive.csproj index f649fcf797..9d3d489032 100644 --- a/src/GitHub.UI.Reactive/GitHub.UI.Reactive.csproj +++ b/src/GitHub.UI.Reactive/GitHub.UI.Reactive.csproj @@ -5,56 +5,49 @@ Debug AnyCPU {158B05E8-FDBC-4D71-B871-C96E28D5ADF5} + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} Library Properties GitHub GitHub.UI.Reactive - v4.5 + 7.3 + v4.6.1 512 - {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 0264ca35 - 4 - true ..\common\GitHubVS.ruleset true - Internal + true true full false - bin\Debug\ DEBUG;TRACE prompt 4 false - ..\common\GitHubVS.ruleset - true - true + bin\Debug\ + + + true + full + false + CODE_ANALYSIS;DEBUG;TRACE + prompt + 4 + true + bin\Debug\ pdbonly true - bin\Release\ TRACE prompt 4 - false - true - true - ..\common\GitHubVS.ruleset - - - ..\..\script\Key.snk - true - false + true + bin\Release\ + - - False - True - ..\..\packages\NullGuard.Fody.1.4.1\Lib\portable-net4+sl4+wp7+win8+MonoAndroid16+MonoTouch40\NullGuard.dll - @@ -92,13 +85,11 @@ - - Key.snk - - + TwoFactorInput.xaml + @@ -184,24 +175,16 @@ {6afe2e2d-6db0-4430-a2ea-f5f5388d2f78} GitHub.Extensions + + {8d73575a-a89f-47cc-b153-b47dd06837f0} + GitHub.Logging + {346384DD-2445-4A28-AF22-B45F3957BD89} GitHub.UI - - - Designer - - - - - - 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.UI/Assets/Controls.xaml b/src/GitHub.UI/Assets/Controls.xaml index 47977c775a..4e0abcc18b 100644 --- a/src/GitHub.UI/Assets/Controls.xaml +++ b/src/GitHub.UI/Assets/Controls.xaml @@ -19,13 +19,6 @@ Styles for standard windows controls --> - - - - - - - + + + + + + + + + + diff --git a/src/GitHub.UI/Assets/Controls/FilterTextBox.xaml b/src/GitHub.UI/Assets/Controls/FilterTextBox.xaml index a379c03530..dc1708811c 100644 --- a/src/GitHub.UI/Assets/Controls/FilterTextBox.xaml +++ b/src/GitHub.UI/Assets/Controls/FilterTextBox.xaml @@ -13,9 +13,7 @@ - - @@ -40,7 +38,7 @@ - + @@ -122,7 +121,6 @@ - diff --git a/src/GitHub.UI/Assets/Markdown.xaml b/src/GitHub.UI/Assets/Markdown.xaml new file mode 100644 index 0000000000..ae1d329900 --- /dev/null +++ b/src/GitHub.UI/Assets/Markdown.xaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + diff --git a/src/GitHub.UI/Assets/Styles.xaml b/src/GitHub.UI/Assets/Styles.xaml index 89fce77076..b7c80a3c6d 100644 --- a/src/GitHub.UI/Assets/Styles.xaml +++ b/src/GitHub.UI/Assets/Styles.xaml @@ -183,7 +183,7 @@ - diff --git a/src/GitHub.UI/Assets/TextBlocks.xaml b/src/GitHub.UI/Assets/TextBlocks.xaml index 26786fd1d3..5e3c635117 100644 --- a/src/GitHub.UI/Assets/TextBlocks.xaml +++ b/src/GitHub.UI/Assets/TextBlocks.xaml @@ -15,7 +15,6 @@ - @@ -71,7 +70,11 @@ - + + + + + + @@ -96,7 +103,6 @@ - diff --git a/src/GitHub.UI/Behaviours/ClosePopupAction.cs b/src/GitHub.UI/Behaviours/ClosePopupAction.cs new file mode 100644 index 0000000000..382fbb75e0 --- /dev/null +++ b/src/GitHub.UI/Behaviours/ClosePopupAction.cs @@ -0,0 +1,13 @@ +using System.Windows.Controls.Primitives; +using System.Windows.Interactivity; + +namespace GitHub.UI +{ + public class ClosePopupAction : TargetedTriggerAction + { + protected override void Invoke(object parameter) + { + Target.IsOpen = false; + } + } +} \ No newline at end of file diff --git a/src/GitHub.UI/Behaviours/OpenPopupAction.cs b/src/GitHub.UI/Behaviours/OpenPopupAction.cs new file mode 100644 index 0000000000..8a3a63ca3d --- /dev/null +++ b/src/GitHub.UI/Behaviours/OpenPopupAction.cs @@ -0,0 +1,13 @@ +using System.Windows.Controls.Primitives; +using System.Windows.Interactivity; + +namespace GitHub.UI +{ + public class OpenPopupAction : TargetedTriggerAction + { + protected override void Invoke(object parameter) + { + Target.IsOpen = true; + } + } +} \ No newline at end of file diff --git a/src/GitHub.UI/Controls/AppendingPathTextBox.cs b/src/GitHub.UI/Controls/AppendingPathTextBox.cs index 8ebc340dcd..52299831c4 100644 --- a/src/GitHub.UI/Controls/AppendingPathTextBox.cs +++ b/src/GitHub.UI/Controls/AppendingPathTextBox.cs @@ -1,7 +1,6 @@ using System; using System.Windows; using System.Windows.Controls; -using NullGuard; namespace GitHub.UI { @@ -12,14 +11,12 @@ public class AppendingPathTextBox : TextBox public string ParentFolderPath { - [return: AllowNull] get { return (string)GetValue(ParentFolderPathProperty); } set { SetValue(ParentFolderPathProperty, value); } } public string ChildFolderName { - [return: AllowNull] get { return (string)GetValue(ChildFolderNameProperty); } set { SetValue(ChildFolderNameProperty, value); } } @@ -29,7 +26,6 @@ public string ChildFolderName public Visibility PathSeparatorVisibility { - [return: AllowNull] get { return (Visibility)GetValue(PathSeparatorVisibilityProperty); } set { SetValue(PathSeparatorVisibilityProperty, value); } } @@ -39,7 +35,6 @@ public Visibility PathSeparatorVisibility public Visibility ChildFolderVisibility { - [return: AllowNull] get { return (Visibility)GetValue(ChildFolderVisibilityProperty); } set { SetValue(ChildFolderVisibilityProperty, value); } } diff --git a/src/GitHub.UI/Controls/Buttons/GitHubActionLink.cs b/src/GitHub.UI/Controls/Buttons/GitHubActionLink.cs new file mode 100644 index 0000000000..a7b0f3d0bf --- /dev/null +++ b/src/GitHub.UI/Controls/Buttons/GitHubActionLink.cs @@ -0,0 +1,39 @@ +using System.Windows; +using System.Windows.Controls; + +namespace GitHub.UI +{ + public partial class GitHubActionLink : Button + { + public static readonly DependencyProperty HasDropDownProperty = DependencyProperty.Register( + "HasDropDown", typeof(bool), typeof(GitHubActionLink)); + + public static readonly DependencyProperty TextTrimmingProperty = + TextBlock.TextTrimmingProperty.AddOwner(typeof(GitHubActionLink)); + + public static readonly DependencyProperty TextWrappingProperty = + TextBlock.TextWrappingProperty.AddOwner(typeof(GitHubActionLink)); + + public bool HasDropDown + { + get { return (bool)GetValue(HasDropDownProperty); } + set { SetValue(HasDropDownProperty, value); } + } + + public TextTrimming TextTrimming + { + get { return (TextTrimming)GetValue(TextTrimmingProperty); } + set { SetValue(TextTrimmingProperty, value); } + } + + public TextWrapping TextWrapping + { + get { return (TextWrapping)GetValue(TextWrappingProperty); } + set { SetValue(TextWrappingProperty, value); } + } + + public GitHubActionLink() + { + } + } +} diff --git a/src/GitHub.UI/Controls/Buttons/OcticonButton.cs b/src/GitHub.UI/Controls/Buttons/OcticonButton.cs index 8d9b152799..6ecd689870 100644 --- a/src/GitHub.UI/Controls/Buttons/OcticonButton.cs +++ b/src/GitHub.UI/Controls/Buttons/OcticonButton.cs @@ -4,10 +4,12 @@ using System.Text; using System.Windows; using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Shapes; namespace GitHub.UI { - public class OcticonButton: Button + public class OcticonButton : Button { public static readonly DependencyProperty IconRotationAngleProperty = DependencyProperty.Register( "IconRotationAngle", typeof(double), typeof(OcticonButton), @@ -18,5 +20,34 @@ public double IconRotationAngle get { return (double)GetValue(IconRotationAngleProperty); } set { SetValue(IconRotationAngleProperty, value); } } + + public static DependencyProperty DataProperty = + Path.DataProperty.AddOwner(typeof(OcticonButton)); + + public Geometry Data + { + get { return (Geometry)GetValue(DataProperty); } + set { SetValue(DataProperty, value); } + } + + public static DependencyProperty IconProperty = + OcticonPath.IconProperty.AddOwner( + typeof(OcticonButton), + new FrameworkPropertyMetadata(defaultValue: Octicon.mark_github, flags: + FrameworkPropertyMetadataOptions.AffectsArrange | + FrameworkPropertyMetadataOptions.AffectsMeasure | + FrameworkPropertyMetadataOptions.AffectsRender, + propertyChangedCallback: OnIconChanged)); + + public Octicon Icon + { + get { return (Octicon)GetValue(OcticonPath.IconProperty); } + set { SetValue(OcticonPath.IconProperty, value); } + } + + static void OnIconChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + d.SetValue(DataProperty, OcticonPath.GetGeometryForIcon((Octicon)e.NewValue)); + } } } diff --git a/src/GitHub.UI/Controls/Buttons/OcticonButton.xaml b/src/GitHub.UI/Controls/Buttons/OcticonButton.xaml new file mode 100644 index 0000000000..dfbb963c87 --- /dev/null +++ b/src/GitHub.UI/Controls/Buttons/OcticonButton.xaml @@ -0,0 +1,106 @@ + + + + + + \ No newline at end of file diff --git a/src/GitHub.UI/Controls/FilterTextBox.cs b/src/GitHub.UI/Controls/FilterTextBox.cs index 7ffc7ee209..dafd704731 100644 --- a/src/GitHub.UI/Controls/FilterTextBox.cs +++ b/src/GitHub.UI/Controls/FilterTextBox.cs @@ -4,7 +4,6 @@ using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; -using NullGuard; namespace GitHub.UI { @@ -17,7 +16,6 @@ public class FilterTextBox : TextBox [DefaultValue("Filter")] public string PromptText { - [return: AllowNull] get { return (string)GetValue(PromptTextProperty); } set { SetValue(PromptTextProperty, value); } } diff --git a/src/GitHub.UI/Controls/Octicons/OcticonImage.cs b/src/GitHub.UI/Controls/Octicons/OcticonImage.cs index a2937631bc..62484addad 100644 --- a/src/GitHub.UI/Controls/Octicons/OcticonImage.cs +++ b/src/GitHub.UI/Controls/Octicons/OcticonImage.cs @@ -1,6 +1,5 @@ using System.Windows; using System.Windows.Controls; -using NullGuard; namespace GitHub.UI { @@ -8,7 +7,6 @@ public class OcticonImage : Control { public Octicon Icon { - [return: AllowNull] get { return (Octicon)GetValue(OcticonPath.IconProperty); } set { SetValue(OcticonPath.IconProperty, value); } } diff --git a/src/GitHub.UI/Controls/Octicons/OcticonImage.xaml b/src/GitHub.UI/Controls/Octicons/OcticonImage.xaml index b8a739a366..54d7cba55d 100644 --- a/src/GitHub.UI/Controls/Octicons/OcticonImage.xaml +++ b/src/GitHub.UI/Controls/Octicons/OcticonImage.xaml @@ -5,7 +5,6 @@ diff --git a/src/GitHub.UI/Controls/ToggleButtons/OcticonToggleButton.cs b/src/GitHub.UI/Controls/ToggleButtons/OcticonToggleButton.cs index 96a6011661..2697dc25ba 100644 --- a/src/GitHub.UI/Controls/ToggleButtons/OcticonToggleButton.cs +++ b/src/GitHub.UI/Controls/ToggleButtons/OcticonToggleButton.cs @@ -1,7 +1,6 @@ using System.Windows; using System.Windows.Controls.Primitives; using System.Windows.Media; -using NullGuard; namespace GitHub.UI { @@ -53,42 +52,36 @@ public abstract class OcticonToggleButton: ToggleButton public Octicon IconChecked { - [return: AllowNull] get { return (Octicon)GetValue(IconCheckedProperty); } set { SetValue(IconCheckedProperty, value); } } public Geometry PathChecked { - [return: AllowNull] get { return (Geometry)GetValue(PathCheckedProperty); } set { SetValue(PathCheckedProperty, value); } } public Octicon IconUnchecked { - [return: AllowNull] get { return (Octicon)GetValue(IconUncheckedProperty); } set { SetValue(IconUncheckedProperty, value); } } public Geometry PathUnchecked { - [return: AllowNull] get { return (Geometry)GetValue(PathUncheckedProperty); } set { SetValue(PathUncheckedProperty, value); } } public Octicon IconIndeterminate { - [return: AllowNull] get { return (Octicon)GetValue(IconIndeterminateProperty); } set { SetValue(IconIndeterminateProperty, value); } } public Geometry PathIndeterminate { - [return: AllowNull] get { return (Geometry)GetValue(PathIndeterminateProperty); } set { SetValue(PathIndeterminateProperty, value); } } diff --git a/src/GitHub.UI/Controls/TrimmedPathTextBlock.cs b/src/GitHub.UI/Controls/TrimmedPathTextBlock.cs new file mode 100644 index 0000000000..84b000c8e6 --- /dev/null +++ b/src/GitHub.UI/Controls/TrimmedPathTextBlock.cs @@ -0,0 +1,166 @@ +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace GitHub.UI +{ + /// + /// TextBlock that displays a path and intelligently trims with ellipsis when the path doesn't + /// fit in the allocated size. + /// + /// + /// When displaying a path that is too long for its allocated space, we need to trim the path + /// with ellipses intelligently instead of simply trimming the end (as this is the filename + /// which is the most important part!). This control trims a path in the following manner with + /// decreasing allocated space: + /// + /// - VisualStudio\src\GitHub.UI\Controls\TrimmedPathTextBlock.cs + /// - VisualStudio\...\GitHub.UI\Controls\TrimmedPathTextBlock.cs + /// - VisualStudio\...\...\Controls\TrimmedPathTextBlock.cs + /// - VisualStudio\...\...\...\TrimmedPathTextBlock.cs + /// - ...\...\...\...\TrimmedPathTextBlock.cs + /// + public class TrimmedPathTextBlock : FrameworkElement + { + public static readonly DependencyProperty FontFamilyProperty = + TextBlock.FontFamilyProperty.AddOwner(typeof(TrimmedPathTextBlock)); + public static readonly DependencyProperty FontSizeProperty = + TextBlock.FontSizeProperty.AddOwner(typeof(TrimmedPathTextBlock)); + public static readonly DependencyProperty FontStretchProperty = + TextBlock.FontStretchProperty.AddOwner(typeof(TrimmedPathTextBlock)); + public static readonly DependencyProperty FontStyleProperty = + TextBlock.FontStyleProperty.AddOwner(typeof(TrimmedPathTextBlock)); + public static readonly DependencyProperty FontWeightProperty = + TextBlock.FontWeightProperty.AddOwner(typeof(TrimmedPathTextBlock)); + public static readonly DependencyProperty ForegroundProperty = + TextBlock.ForegroundProperty.AddOwner(typeof(TrimmedPathTextBlock)); + public static readonly DependencyProperty TextProperty = + TextBlock.TextProperty.AddOwner( + typeof(TrimmedPathTextBlock), + new FrameworkPropertyMetadata( + null, + FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender, + TextChanged)); + + FormattedText formattedText; + FormattedText renderText; + + public FontFamily FontFamily + { + get { return (FontFamily)GetValue(FontFamilyProperty); } + set { SetValue(FontFamilyProperty, value); } + } + + public double FontSize + { + get { return (double)GetValue(FontSizeProperty); } + set { SetValue(FontSizeProperty, value); } + } + + public FontStretch FontStretch + { + get { return (FontStretch)GetValue(FontStretchProperty); } + set { SetValue(FontStretchProperty, value); } + } + + public FontStyle FontStyle + { + get { return (FontStyle)GetValue(FontStyleProperty); } + set { SetValue(FontStyleProperty, value); } + } + + public FontWeight FontWeight + { + get { return (FontWeight)GetValue(FontWeightProperty); } + set { SetValue(FontWeightProperty, value); } + } + + public Brush Foreground + { + get { return (Brush)GetValue(ForegroundProperty); } + set { SetValue(ForegroundProperty, value); } + } + + public string Text + { + get { return (string)GetValue(TextProperty); } + set { SetValue(TextProperty, value); } + } + + protected FormattedText FormattedText + { + get + { + if (formattedText == null && Text != null) + { + formattedText = CreateFormattedText(Text); + } + + return formattedText; + } + } + + protected override Size MeasureOverride(Size availableSize) + { + if (Text == null) + { + return new Size(); + } + + var parts = Text + .Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }) + .ToList(); + var nextPart = Math.Min(1, parts.Count - 1); + + while (true) + { + renderText = CreateFormattedText(string.Join(Path.DirectorySeparatorChar.ToString(), parts)); + + if (renderText.Width <= availableSize.Width || nextPart == -1) + break; + + parts[nextPart] = "\u2026"; + + if (nextPart == 0) + nextPart = -1; + else if (nextPart == parts.Count - 2) + nextPart = 0; + else + nextPart++; + }; + + return new Size(renderText.Width, renderText.Height); + } + + protected override void OnRender(DrawingContext drawingContext) + { + drawingContext.DrawText(renderText, new Point()); + } + + FormattedText CreateFormattedText(string text) + { + return new FormattedText( + text, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), + FontSize, + Foreground); + } + + static void TextChanged(object sender, DependencyPropertyChangedEventArgs e) + { + var textBlock = sender as TrimmedPathTextBlock; + + if (textBlock != null) + { + textBlock.formattedText = null; + textBlock.renderText = null; + } + } + } +} diff --git a/src/GitHub.UI/Converters/AllCapsConverter.cs b/src/GitHub.UI/Converters/AllCapsConverter.cs new file mode 100644 index 0000000000..5d06f88dab --- /dev/null +++ b/src/GitHub.UI/Converters/AllCapsConverter.cs @@ -0,0 +1,19 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace GitHub.UI +{ + public class AllCapsConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value.ToString().ToUpper(CultureInfo.CurrentCulture); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/GitHub.UI/Converters/BooleanToHiddenVisibilityConverter.cs b/src/GitHub.UI/Converters/BooleanToHiddenVisibilityConverter.cs index 1b175a3a5d..46c059bd11 100644 --- a/src/GitHub.UI/Converters/BooleanToHiddenVisibilityConverter.cs +++ b/src/GitHub.UI/Converters/BooleanToHiddenVisibilityConverter.cs @@ -1,7 +1,6 @@ using System; using System.Globalization; using System.Windows; -using NullGuard; namespace GitHub.UI { @@ -10,17 +9,17 @@ public sealed class BooleanToHiddenVisibilityConverter : ValueConverterMarkupExt { public override object Convert( object value, - [AllowNull]Type targetType, - [AllowNull]object parameter, - [AllowNull]CultureInfo culture) + Type targetType, + object parameter, + CultureInfo culture) { return value is bool && (bool)value ? Visibility.Visible : Visibility.Hidden; } public override object ConvertBack(object value, - [AllowNull]Type targetType, - [AllowNull]object parameter, - [AllowNull]CultureInfo culture) + Type targetType, + object parameter, + CultureInfo culture) { return value is Visibility && (Visibility)value == Visibility.Visible; } diff --git a/src/GitHub.UI/Converters/BooleanToInverseHiddenVisibilityConverter.cs b/src/GitHub.UI/Converters/BooleanToInverseHiddenVisibilityConverter.cs index b2e5a1137e..f1f62d8dbd 100644 --- a/src/GitHub.UI/Converters/BooleanToInverseHiddenVisibilityConverter.cs +++ b/src/GitHub.UI/Converters/BooleanToInverseHiddenVisibilityConverter.cs @@ -1,7 +1,6 @@ using System; using System.Globalization; using System.Windows; -using NullGuard; namespace GitHub.UI { @@ -9,17 +8,17 @@ namespace GitHub.UI public sealed class BooleanToInverseHiddenVisibilityConverter : ValueConverterMarkupExtension { public override object Convert(object value, - [AllowNull]Type targetType, - [AllowNull]object parameter, - [AllowNull]CultureInfo culture) + Type targetType, + object parameter, + CultureInfo culture) { return value is bool && (bool)value ? Visibility.Hidden : Visibility.Visible; } public override object ConvertBack(object value, - [AllowNull]Type targetType, - [AllowNull]object parameter, - [AllowNull]CultureInfo culture) + Type targetType, + object parameter, + CultureInfo culture) { return value is Visibility && (Visibility)value != Visibility.Visible; } diff --git a/src/GitHub.UI/Converters/BooleanToInverseVisibilityConverter.cs b/src/GitHub.UI/Converters/BooleanToInverseVisibilityConverter.cs index c29360926a..5a3ac549dd 100644 --- a/src/GitHub.UI/Converters/BooleanToInverseVisibilityConverter.cs +++ b/src/GitHub.UI/Converters/BooleanToInverseVisibilityConverter.cs @@ -1,7 +1,6 @@ using System; using System.Globalization; using System.Windows; -using NullGuard; namespace GitHub.UI { @@ -9,17 +8,17 @@ namespace GitHub.UI public sealed class BooleanToInverseVisibilityConverter : ValueConverterMarkupExtension { public override object Convert(object value, - [AllowNull]Type targetType, - [AllowNull]object parameter, - [AllowNull]CultureInfo culture) + Type targetType, + object parameter, + CultureInfo culture) { return value is bool && (bool)value ? Visibility.Collapsed : Visibility.Visible; } public override object ConvertBack(object value, - [AllowNull]Type targetType, - [AllowNull]object parameter, - [AllowNull]CultureInfo culture) + Type targetType, + object parameter, + CultureInfo culture) { return value is Visibility && (Visibility)value != Visibility.Visible; } diff --git a/src/GitHub.UI/Converters/BooleanToVisibilityConverter.cs b/src/GitHub.UI/Converters/BooleanToVisibilityConverter.cs index 3355a05f35..77b8905181 100644 --- a/src/GitHub.UI/Converters/BooleanToVisibilityConverter.cs +++ b/src/GitHub.UI/Converters/BooleanToVisibilityConverter.cs @@ -1,7 +1,6 @@ using System; using System.Globalization; using System.Windows; -using NullGuard; namespace GitHub.UI { @@ -11,17 +10,17 @@ public sealed class BooleanToVisibilityConverter : ValueConverterMarkupExtension readonly System.Windows.Controls.BooleanToVisibilityConverter converter = new System.Windows.Controls.BooleanToVisibilityConverter(); public override object Convert(object value, - [AllowNull]Type targetType, - [AllowNull]object parameter, - [AllowNull]CultureInfo culture) + Type targetType, + object parameter, + CultureInfo culture) { return converter.Convert(value, targetType, parameter, culture); } public override object ConvertBack(object value, - [AllowNull]Type targetType, - [AllowNull]object parameter, - [AllowNull]CultureInfo culture) + Type targetType, + object parameter, + CultureInfo culture) { return converter.ConvertBack(value, targetType, parameter, culture); } diff --git a/src/GitHub.UI/Converters/BranchNameConverter.cs b/src/GitHub.UI/Converters/BranchNameConverter.cs new file mode 100644 index 0000000000..0db0f51c3e --- /dev/null +++ b/src/GitHub.UI/Converters/BranchNameConverter.cs @@ -0,0 +1,44 @@ +using GitHub.Models; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Data; + +namespace GitHub.UI.Converters +{ + public class BranchNameConverter : IMultiValueConverter + { + + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + var branch = values.OfType().FirstOrDefault(); + var activeRepo = values.OfType().FirstOrDefault(); + + if (branch != null && activeRepo != null) + { + var repo = (IRemoteRepositoryModel)branch.Repository; + + if (repo.Parent == null && activeRepo.Owner != repo.Owner) + { + return repo.Owner + ":" + branch.Name; + } + + return branch.DisplayName; + } + else + { + return values.FirstOrDefault()?.ToString(); + } + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + return null; + } + + } +} diff --git a/src/GitHub.UI/Converters/CountToVisibilityConverter.cs b/src/GitHub.UI/Converters/CountToVisibilityConverter.cs new file mode 100644 index 0000000000..366e5d0da0 --- /dev/null +++ b/src/GitHub.UI/Converters/CountToVisibilityConverter.cs @@ -0,0 +1,19 @@ +using System; +using System.Globalization; +using System.Windows; + +namespace GitHub.UI +{ + /// + /// Convert a count to visibility based on the following rule: + /// * If count == 0, return Visibility.Visible + /// * If count > 0, return Visibility.Collapsed + /// + public class CountToVisibilityConverter : ValueConverterMarkupExtension + { + public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return ((int)value > 0) ? Visibility.Visible : Visibility.Collapsed; + } + } +} diff --git a/src/GitHub.UI/Converters/DefaultValueConverter.cs b/src/GitHub.UI/Converters/DefaultValueConverter.cs new file mode 100644 index 0000000000..cc8267c684 --- /dev/null +++ b/src/GitHub.UI/Converters/DefaultValueConverter.cs @@ -0,0 +1,14 @@ +using System; +using System.Diagnostics; +using System.Globalization; + +namespace GitHub.UI +{ + public class DefaultValueConverter : ValueConverterMarkupExtension + { + public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value ?? parameter; + } + } +} diff --git a/src/GitHub.UI/Converters/DurationToStringConverter.cs b/src/GitHub.UI/Converters/DurationToStringConverter.cs new file mode 100644 index 0000000000..dbebe9f3fa --- /dev/null +++ b/src/GitHub.UI/Converters/DurationToStringConverter.cs @@ -0,0 +1,46 @@ +using System; +using System.Globalization; + +namespace GitHub.UI +{ + public class DurationToStringConverter : ValueConverterMarkupExtension + { + public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + TimeSpan duration; + if (value is TimeSpan span) + duration = span; + else if (value is DateTime time) + duration = DateTime.UtcNow - time; + else if (value is DateTimeOffset offset) + duration = DateTimeOffset.UtcNow - offset; + else + return value; + + if (duration.Ticks <= 0) + { + return Resources.JustNow; + } + + const int year = 365; + const int month = 30; + const int day = 24; + const int hour = 60; + const int minute = 60; + + if (duration.TotalDays >= year) + return string.Format(culture, (int)(duration.TotalDays / year) > 1 ? Resources.years : Resources.year, (int)(duration.TotalDays / year)); + else if (duration.TotalDays >= 360) + return string.Format(culture, Resources.months, 11); + else if (duration.TotalDays >= month) + return string.Format(culture, (int)(duration.TotalDays / (month)) > 1 ? Resources.months : Resources.month, (int)(duration.TotalDays / (month))); + else if (duration.TotalHours >= day) + return string.Format(culture, (int)(duration.TotalHours / day) > 1 ? Resources.days : Resources.day, (int)(duration.TotalHours / day)); + else if (duration.TotalMinutes >= hour) + return string.Format(culture, (int)(duration.TotalMinutes / hour) > 1 ? Resources.hours : Resources.hour, (int)(duration.TotalMinutes / hour)); + else if (duration.TotalSeconds >= minute) + return string.Format(culture, (int)(duration.TotalSeconds / minute) > 1 ? Resources.minutes : Resources.minute, (int)(duration.TotalSeconds / minute)); + return string.Format(culture, duration.TotalSeconds > 1 || duration.Ticks == 0 ? Resources.seconds : Resources.second, duration.TotalSeconds); + } + } +} \ No newline at end of file diff --git a/src/GitHub.UI/Converters/EqualityConverter.cs b/src/GitHub.UI/Converters/EqualityConverter.cs new file mode 100644 index 0000000000..4750aa65f2 --- /dev/null +++ b/src/GitHub.UI/Converters/EqualityConverter.cs @@ -0,0 +1,33 @@ +using System; +using System.Globalization; +using System.Windows; + +namespace GitHub.UI +{ + [Localizability(LocalizationCategory.NeverLocalize)] + public sealed class EqualityConverter : MultiValueConverterMarkupExtension + { + public override object Convert( + object[] value, + Type targetType, + object parameter, + CultureInfo culture) + { + if (value.Length == 2) + { + return Equals(value[0], value[1]); + } + + return false; + } + + public override object[] ConvertBack( + object value, + Type[] targetType, + object parameter, + CultureInfo culture) + { + return null; + } + } +} diff --git a/src/GitHub.UI/Converters/EqualsToVisibilityConverter.cs b/src/GitHub.UI/Converters/EqualsToVisibilityConverter.cs new file mode 100644 index 0000000000..e26403b0da --- /dev/null +++ b/src/GitHub.UI/Converters/EqualsToVisibilityConverter.cs @@ -0,0 +1,30 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; +using System.Windows.Markup; + +namespace GitHub.UI +{ + public class EqualsToVisibilityConverter : MarkupExtension, IValueConverter + { + readonly string visibleValue; + + public EqualsToVisibilityConverter(string visibleValue) + { + this.visibleValue = visibleValue; + } + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value?.ToString() == visibleValue ? Visibility.Visible : Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + + public override object ProvideValue(IServiceProvider serviceProvider) => this; + } +} diff --git a/src/GitHub.UI/Converters/InverseBooleanConverter.cs b/src/GitHub.UI/Converters/InverseBooleanConverter.cs index 13dc95c879..660430f1c3 100644 --- a/src/GitHub.UI/Converters/InverseBooleanConverter.cs +++ b/src/GitHub.UI/Converters/InverseBooleanConverter.cs @@ -2,7 +2,6 @@ using System.Globalization; using System.Windows; using System.Windows.Data; -using NullGuard; namespace GitHub.UI { @@ -12,8 +11,8 @@ public class InverseBooleanConverter : ValueConverterMarkupExtension + { + public override object Convert( + object[] value, + Type targetType, + object parameter, + CultureInfo culture) + { + return value.OfType().All(x => x) ? Visibility.Visible : Visibility.Collapsed; + } + + public override object[] ConvertBack( + object value, + Type[] targetType, + object parameter, + CultureInfo culture) + { + return null; + } + } +} diff --git a/src/GitHub.UI/Converters/NotEqualsToVisibilityConverter.cs b/src/GitHub.UI/Converters/NotEqualsToVisibilityConverter.cs new file mode 100644 index 0000000000..42d186302b --- /dev/null +++ b/src/GitHub.UI/Converters/NotEqualsToVisibilityConverter.cs @@ -0,0 +1,30 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; +using System.Windows.Markup; + +namespace GitHub.UI +{ + public class NotEqualsToVisibilityConverter : MarkupExtension, IValueConverter + { + readonly string collapsedValue; + + public NotEqualsToVisibilityConverter(string collapsedValue) + { + this.collapsedValue = collapsedValue; + } + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value?.ToString() != collapsedValue ? Visibility.Visible : Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + + public override object ProvideValue(IServiceProvider serviceProvider) => this; + } +} diff --git a/src/GitHub.UI/Converters/NullToVisibilityConverter.cs b/src/GitHub.UI/Converters/NullToVisibilityConverter.cs new file mode 100644 index 0000000000..3f54a541f1 --- /dev/null +++ b/src/GitHub.UI/Converters/NullToVisibilityConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Globalization; +using System.Windows; + +namespace GitHub.UI +{ + [Localizability(LocalizationCategory.NeverLocalize)] + public sealed class NullToVisibilityConverter : ValueConverterMarkupExtension + { + readonly System.Windows.Controls.BooleanToVisibilityConverter converter = new System.Windows.Controls.BooleanToVisibilityConverter(); + + public override object Convert(object value, + Type targetType, + object parameter, + CultureInfo culture) + { + return converter.Convert(value != null, targetType, parameter, culture); + } + + public override object ConvertBack(object value, + Type targetType, + object parameter, + CultureInfo culture) + { + return converter.ConvertBack(value != null, targetType, parameter, culture); + } + } +} diff --git a/src/GitHub.UI/Converters/StickieListItemConverter.cs b/src/GitHub.UI/Converters/StickieListItemConverter.cs new file mode 100644 index 0000000000..9652f1e208 --- /dev/null +++ b/src/GitHub.UI/Converters/StickieListItemConverter.cs @@ -0,0 +1,13 @@ +using System; +using System.Globalization; + +namespace GitHub.UI +{ + public class StickieListItemConverter : MultiValueConverterMarkupExtension + { + public override object Convert(object[] value, Type targetType, object parameter, CultureInfo culture) + { + return value[1]; + } + } +} \ No newline at end of file diff --git a/src/GitHub.UI/Converters/StringConverter.cs b/src/GitHub.UI/Converters/StringConverter.cs index 5289b6279b..958650f3a6 100644 --- a/src/GitHub.UI/Converters/StringConverter.cs +++ b/src/GitHub.UI/Converters/StringConverter.cs @@ -10,7 +10,6 @@ public override object Convert(object value, Type targetType, object parameter, { var text = value as string; if (String.IsNullOrEmpty(text)) return null; - Debug.Assert(text != null, "This should not be null. Possible error with IsNullOrEmpty extension method"); var conversionType = GetConversionTypeFromParameter(parameter); diff --git a/src/GitHub.UI/Converters/ThicknessConverter.cs b/src/GitHub.UI/Converters/ThicknessConverter.cs index 5121d32949..2edad6a235 100644 --- a/src/GitHub.UI/Converters/ThicknessConverter.cs +++ b/src/GitHub.UI/Converters/ThicknessConverter.cs @@ -1,7 +1,6 @@ using System; using System.Windows; using System.Windows.Data; -using NullGuard; namespace GitHub.UI { @@ -9,9 +8,9 @@ public class ThicknessConverter : IValueConverter { public object Convert( object value, - [AllowNull]Type targetType, - [AllowNull]object parameter, - [AllowNull]System.Globalization.CultureInfo culture) + Type targetType, + object parameter, + System.Globalization.CultureInfo culture) { var t = ((Thickness)value); diff --git a/src/GitHub.UI/Converters/TrimNewlinesConverter.cs b/src/GitHub.UI/Converters/TrimNewlinesConverter.cs new file mode 100644 index 0000000000..d7dee09fd7 --- /dev/null +++ b/src/GitHub.UI/Converters/TrimNewlinesConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace GitHub.UI +{ + /// + /// An that trims newlines and tabs from a string and replaces them + /// with spaces. + /// + public class TrimNewlinesConverter : ValueConverterMarkupExtension + { + public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var text = value as string; + if (String.IsNullOrEmpty(text)) return null; + return Regex.Replace(text, @"\t|\n|\r", " "); + } + } +} diff --git a/src/GitHub.UI/FodyWeavers.xml b/src/GitHub.UI/FodyWeavers.xml deleted file mode 100644 index 95494fb843..0000000000 --- a/src/GitHub.UI/FodyWeavers.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/GitHub.UI/GitHub.UI.csproj b/src/GitHub.UI/GitHub.UI.csproj index 47647dd1a3..041f14a08d 100644 --- a/src/GitHub.UI/GitHub.UI.csproj +++ b/src/GitHub.UI/GitHub.UI.csproj @@ -5,56 +5,69 @@ Debug AnyCPU {346384DD-2445-4A28-AF22-B45F3957BD89} + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} Library Properties GitHub.UI GitHub.UI - v4.5 + 7.3 + v4.6.1 512 - {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 6e7ecf50 - Internal + ..\common\GitHubVS.ruleset + true + true true full false - bin\Debug\ DEBUG;TRACE prompt 4 false - ..\common\GitHubVS.ruleset - true - true + bin\Debug\ + + + true + full + false + CODE_ANALYSIS;DEBUG;TRACE + prompt + 4 + true + bin\Debug\ pdbonly true - bin\Release\ TRACE prompt 4 - false - true - true - ..\common\GitHubVS.ruleset - - - ..\..\script\Key.snk - true - false + true + bin\Release\ + - - - False - ..\..\packages\NullGuard.Fody.1.4.1\Lib\portable-net4+sl4+wp7+win8+MonoAndroid16+MonoTouch40\NullGuard.dll + + ..\..\packages\Expression.Blend.Sdk.WPF.1.0.1\lib\net45\Microsoft.Expression.Interactions.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Shell.14.0.14.3.25407\lib\Microsoft.VisualStudio.Shell.14.0.dll + True + + ..\..\packages\Serilog.2.5.0\lib\net46\Serilog.dll + True + + + ..\..\packages\Expression.Blend.Sdk.WPF.1.0.1\lib\net45\System.Windows.Interactivity.dll + True + @@ -64,15 +77,49 @@ + + + + True True OcticonPaths.resx - - Key.snk - + + + + Spinner.xaml + + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + True + True + AutomationIDs.resx + + @@ -95,6 +142,7 @@ + @@ -121,7 +169,6 @@ - @@ -145,6 +192,9 @@ MSBuild:Compile Designer + + Designer + MSBuild:Compile Designer @@ -165,10 +215,18 @@ MSBuild:Compile Designer + + MSBuild:Compile + Designer + MSBuild:Compile Designer + + MSBuild:Compile + Designer + MSBuild:Compile @@ -176,6 +234,10 @@ MSBuild:Compile Designer + + MSBuild:Compile + Designer + MSBuild:Compile Designer @@ -187,6 +249,10 @@ MSBuild:Compile + + MSBuild:Compile + Designer + @@ -194,24 +260,34 @@ ResXFileCodeGenerator OcticonPaths.Designer.cs + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + PublicResXFileCodeGenerator + AutomationIDs.Designer.cs + {9aea02db-02b5-409c-b0ca-115d05331a6b} GitHub.Exports + + {6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78} + GitHub.Extensions + + + {8d73575a-a89f-47cc-b153-b47dd06837f0} + GitHub.Logging + - + - - - - 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}. - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + {0:N0} day ago + + + {0:N0} days ago + + + {0:N0} hour ago + + + {0:N0} hours ago + + + just now + + + {0:N0} minute ago + + + {0:N0} minutes ago + + + {0:N0} month ago + + + {0:N0} months ago + + + {0:N0} second ago + + + {0:N0} seconds ago + + + {0:N0} year ago + + + {0:N0} years ago + + \ No newline at end of file diff --git a/src/GitHub.UI/SharedDictionary.xaml b/src/GitHub.UI/SharedDictionary.xaml index d4bd2f9102..14cfd8d63c 100644 --- a/src/GitHub.UI/SharedDictionary.xaml +++ b/src/GitHub.UI/SharedDictionary.xaml @@ -5,5 +5,5 @@ - + \ No newline at end of file diff --git a/src/GitHub.UI/TestAutomation/AutomationIDs.Designer.cs b/src/GitHub.UI/TestAutomation/AutomationIDs.Designer.cs new file mode 100644 index 0000000000..d8bd417d15 --- /dev/null +++ b/src/GitHub.UI/TestAutomation/AutomationIDs.Designer.cs @@ -0,0 +1,1260 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace GitHub.UI.TestAutomation { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class AutomationIDs { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal AutomationIDs() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GitHub.UI.TestAutomation.AutomationIDs", typeof(AutomationIDs).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to AccountComboBox. + /// + public static string AccountComboBox { + get { + return ResourceManager.GetString("AccountComboBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to AccountListBoxItem. + /// + public static string AccountListBoxItem { + get { + return ResourceManager.GetString("AccountListBoxItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CloneAGitHubRepositoryWindow. + /// + public static string CloneAGitHubRepositoryWindow { + get { + return ResourceManager.GetString("CloneAGitHubRepositoryWindow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CloneHyperlink. + /// + public static string CloneHyperlink { + get { + return ResourceManager.GetString("CloneHyperlink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CloneRepositoryButton. + /// + public static string CloneRepositoryButton { + get { + return ResourceManager.GetString("CloneRepositoryButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CloneRepositoryCloseButton. + /// + public static string CloneRepositoryCloseButton { + get { + return ResourceManager.GetString("CloneRepositoryCloseButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CloneRepositoryLocalPathBrowsePathButton. + /// + public static string CloneRepositoryLocalPathBrowsePathButton { + get { + return ResourceManager.GetString("CloneRepositoryLocalPathBrowsePathButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CloneRepositoryLocalPathCustom. + /// + public static string CloneRepositoryLocalPathCustom { + get { + return ResourceManager.GetString("CloneRepositoryLocalPathCustom", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CloneRepositoryTitleBar. + /// + public static string CloneRepositoryTitleBar { + get { + return ResourceManager.GetString("CloneRepositoryTitleBar", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ConnectToGitHubCloseButton. + /// + public static string ConnectToGitHubCloseButton { + get { + return ResourceManager.GetString("ConnectToGitHubCloseButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ConnectToGitHubMenuItem. + /// + public static string ConnectToGitHubMenuItem { + get { + return ResourceManager.GetString("ConnectToGitHubMenuItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ConnectToGitHubTitleBar. + /// + public static string ConnectToGitHubTitleBar { + get { + return ResourceManager.GetString("ConnectToGitHubTitleBar", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ConnectToGitHubWindow. + /// + public static string ConnectToGitHubWindow { + get { + return ResourceManager.GetString("ConnectToGitHubWindow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CreateAGitHubGistControl. + /// + public static string CreateAGitHubGistControl { + get { + return ResourceManager.GetString("CreateAGitHubGistControl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CreateAGitHubGistTitleBar. + /// + public static string CreateAGitHubGistTitleBar { + get { + return ResourceManager.GetString("CreateAGitHubGistTitleBar", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CreateAGitHubRepositoryWindow. + /// + public static string CreateAGitHubRepositoryWindow { + get { + return ResourceManager.GetString("CreateAGitHubRepositoryWindow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CreatedPullRequestAuthorImage. + /// + public static string CreatedPullRequestAuthorImage { + get { + return ResourceManager.GetString("CreatedPullRequestAuthorImage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CreatedPullRequestDetailsTextBlock. + /// + public static string CreatedPullRequestDetailsTextBlock { + get { + return ResourceManager.GetString("CreatedPullRequestDetailsTextBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CreatedPullRequestListItem. + /// + public static string CreatedPullRequestListItem { + get { + return ResourceManager.GetString("CreatedPullRequestListItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CreatedPullRequestNumberHyperlink. + /// + public static string CreatedPullRequestNumberHyperlink { + get { + return ResourceManager.GetString("CreatedPullRequestNumberHyperlink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CreatedPullRequestTitleHyperlink. + /// + public static string CreatedPullRequestTitleHyperlink { + get { + return ResourceManager.GetString("CreatedPullRequestTitleHyperlink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CreateGistButton. + /// + public static string CreateGistButton { + get { + return ResourceManager.GetString("CreateGistButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CreateHyperlink. + /// + public static string CreateHyperlink { + get { + return ResourceManager.GetString("CreateHyperlink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CreateNewHyperlink. + /// + public static string CreateNewHyperlink { + get { + return ResourceManager.GetString("CreateNewHyperlink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CreateRepositoryButton. + /// + public static string CreateRepositoryButton { + get { + return ResourceManager.GetString("CreateRepositoryButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CreateRepositoryCloseButton. + /// + public static string CreateRepositoryCloseButton { + get { + return ResourceManager.GetString("CreateRepositoryCloseButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CreateRepositoryLocalPathBrowseButton. + /// + public static string CreateRepositoryLocalPathBrowseButton { + get { + return ResourceManager.GetString("CreateRepositoryLocalPathBrowseButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CreateRepositoryLocalPathTextBox. + /// + public static string CreateRepositoryLocalPathTextBox { + get { + return ResourceManager.GetString("CreateRepositoryLocalPathTextBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CreateRepositoryTitleBar. + /// + public static string CreateRepositoryTitleBar { + get { + return ResourceManager.GetString("CreateRepositoryTitleBar", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DontHaveDotcomAccountTextBlock. + /// + public static string DontHaveDotcomAccountTextBlock { + get { + return ResourceManager.GetString("DontHaveDotcomAccountTextBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DontHaveEnterpriseTextBlock. + /// + public static string DontHaveEnterpriseTextBlock { + get { + return ResourceManager.GetString("DontHaveEnterpriseTextBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DotcomPasswordTextBox. + /// + public static string DotcomPasswordTextBox { + get { + return ResourceManager.GetString("DotcomPasswordTextBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DotcomSignInButton. + /// + public static string DotcomSignInButton { + get { + return ResourceManager.GetString("DotcomSignInButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DotcomSignUpHyperlink. + /// + public static string DotcomSignUpHyperlink { + get { + return ResourceManager.GetString("DotcomSignUpHyperlink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DotcomUsernameEmailTextBox. + /// + public static string DotcomUsernameEmailTextBox { + get { + return ResourceManager.GetString("DotcomUsernameEmailTextBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to EnterpriseLearnMoreHyperlink. + /// + public static string EnterpriseLearnMoreHyperlink { + get { + return ResourceManager.GetString("EnterpriseLearnMoreHyperlink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to EnterprisePasswordTextBox. + /// + public static string EnterprisePasswordTextBox { + get { + return ResourceManager.GetString("EnterprisePasswordTextBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to EnterpriseServerAddressTextBox. + /// + public static string EnterpriseServerAddressTextBox { + get { + return ResourceManager.GetString("EnterpriseServerAddressTextBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to EnterpriseSignInButton. + /// + public static string EnterpriseSignInButton { + get { + return ResourceManager.GetString("EnterpriseSignInButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to EnterpriseUsernameEmailTextBox. + /// + public static string EnterpriseUsernameEmailTextBox { + get { + return ResourceManager.GetString("EnterpriseUsernameEmailTextBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GistAccountImage. + /// + public static string GistAccountImage { + get { + return ResourceManager.GetString("GistAccountImage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GistAccountNameTextBlock. + /// + public static string GistAccountNameTextBlock { + get { + return ResourceManager.GetString("GistAccountNameTextBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GistCreationControlCustom. + /// + public static string GistCreationControlCustom { + get { + return ResourceManager.GetString("GistCreationControlCustom", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GistDescriptionTextBlock. + /// + public static string GistDescriptionTextBlock { + get { + return ResourceManager.GetString("GistDescriptionTextBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GistDescriptionTextBox. + /// + public static string GistDescriptionTextBox { + get { + return ResourceManager.GetString("GistDescriptionTextBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GistErrorMessageTextBlock. + /// + public static string GistErrorMessageTextBlock { + get { + return ResourceManager.GetString("GistErrorMessageTextBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GistFileNameTextBlock. + /// + public static string GistFileNameTextBlock { + get { + return ResourceManager.GetString("GistFileNameTextBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GistFileNameTextBox. + /// + public static string GistFileNameTextBox { + get { + return ResourceManager.GetString("GistFileNameTextBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GitHubConnectContentCustom. + /// + public static string GitHubConnectContentCustom { + get { + return ResourceManager.GetString("GitHubConnectContentCustom", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GitHubHomeContentCustom. + /// + public static string GitHubHomeContentCustom { + get { + return ResourceManager.GetString("GitHubHomeContentCustom", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GitHubInfoPanel. + /// + public static string GitHubInfoPanel { + get { + return ResourceManager.GetString("GitHubInfoPanel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GitHubInfoPanelCancelButton. + /// + public static string GitHubInfoPanelCancelButton { + get { + return ResourceManager.GetString("GitHubInfoPanelCancelButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GitHubInfoPanelMessageDocument. + /// + public static string GitHubInfoPanelMessageDocument { + get { + return ResourceManager.GetString("GitHubInfoPanelMessageDocument", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GitHubLoggedOutCreateAnAccountHyperlink. + /// + public static string GitHubLoggedOutCreateAnAccountHyperlink { + get { + return ResourceManager.GetString("GitHubLoggedOutCreateAnAccountHyperlink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GitHubLoggedOutSignInHyperlink. + /// + public static string GitHubLoggedOutSignInHyperlink { + get { + return ResourceManager.GetString("GitHubLoggedOutSignInHyperlink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GitHubPaneView. + /// + public static string GitHubPaneView { + get { + return ResourceManager.GetString("GitHubPaneView", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GitHubTabItem. + /// + public static string GitHubTabItem { + get { + return ResourceManager.GetString("GitHubTabItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GitHubToolBar. + /// + public static string GitHubToolBar { + get { + return ResourceManager.GetString("GitHubToolBar", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GitHubToolBarBackImage. + /// + public static string GitHubToolBarBackImage { + get { + return ResourceManager.GetString("GitHubToolBarBackImage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GitHubToolBarForwardImage. + /// + public static string GitHubToolBarForwardImage { + get { + return ResourceManager.GetString("GitHubToolBarForwardImage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GitHubToolBarPullRequestImage. + /// + public static string GitHubToolBarPullRequestImage { + get { + return ResourceManager.GetString("GitHubToolBarPullRequestImage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GitHubToolBarRefreshImage. + /// + public static string GitHubToolBarRefreshImage { + get { + return ResourceManager.GetString("GitHubToolBarRefreshImage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GitignoreComboBox. + /// + public static string GitignoreComboBox { + get { + return ResourceManager.GetString("GitignoreComboBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GitignoreFilterTextBox. + /// + public static string GitignoreFilterTextBox { + get { + return ResourceManager.GetString("GitignoreFilterTextBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GitignoreListBoxItem. + /// + public static string GitignoreListBoxItem { + get { + return ResourceManager.GetString("GitignoreListBoxItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GitignorePopupWindow. + /// + public static string GitignorePopupWindow { + get { + return ResourceManager.GetString("GitignorePopupWindow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GitignoreTextBlock. + /// + public static string GitignoreTextBlock { + get { + return ResourceManager.GetString("GitignoreTextBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LicenseComboBox. + /// + public static string LicenseComboBox { + get { + return ResourceManager.GetString("LicenseComboBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LicenseFilterTextBox. + /// + public static string LicenseFilterTextBox { + get { + return ResourceManager.GetString("LicenseFilterTextBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LicenseListBoxItem. + /// + public static string LicenseListBoxItem { + get { + return ResourceManager.GetString("LicenseListBoxItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LicensePopupWindow. + /// + public static string LicensePopupWindow { + get { + return ResourceManager.GetString("LicensePopupWindow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LicenseTextBlock. + /// + public static string LicenseTextBlock { + get { + return ResourceManager.GetString("LicenseTextBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LoggedOutViewCustom. + /// + public static string LoggedOutViewCustom { + get { + return ResourceManager.GetString("LoggedOutViewCustom", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PowerfulCollaborationTextBlock. + /// + public static string PowerfulCollaborationTextBlock { + get { + return ResourceManager.GetString("PowerfulCollaborationTextBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PrivateGistCheckBox. + /// + public static string PrivateGistCheckBox { + get { + return ResourceManager.GetString("PrivateGistCheckBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PrivateRepositoryCheckBox. + /// + public static string PrivateRepositoryCheckBox { + get { + return ResourceManager.GetString("PrivateRepositoryCheckBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PullRequestCreationCancelButton. + /// + public static string PullRequestCreationCancelButton { + get { + return ResourceManager.GetString("PullRequestCreationCancelButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PullRequestCreationCreateButton. + /// + public static string PullRequestCreationCreateButton { + get { + return ResourceManager.GetString("PullRequestCreationCreateButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PullRequestCreationDescriptionTextBox. + /// + public static string PullRequestCreationDescriptionTextBox { + get { + return ResourceManager.GetString("PullRequestCreationDescriptionTextBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PullRequestCreationDetailPane. + /// + public static string PullRequestCreationDetailPane { + get { + return ResourceManager.GetString("PullRequestCreationDetailPane", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PullRequestCreationTitleTextBox. + /// + public static string PullRequestCreationTitleTextBox { + get { + return ResourceManager.GetString("PullRequestCreationTitleTextBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PullRequestCreationViewCustom. + /// + public static string PullRequestCreationViewCustom { + get { + return ResourceManager.GetString("PullRequestCreationViewCustom", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PullRequestListAssigneeFilterComboBox. + /// + public static string PullRequestListAssigneeFilterComboBox { + get { + return ResourceManager.GetString("PullRequestListAssigneeFilterComboBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PullRequestListAuthorFilterComboBox. + /// + public static string PullRequestListAuthorFilterComboBox { + get { + return ResourceManager.GetString("PullRequestListAuthorFilterComboBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PullRequestListFilterAssigneesDropDown. + /// + public static string PullRequestListFilterAssigneesDropDown { + get { + return ResourceManager.GetString("PullRequestListFilterAssigneesDropDown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PullRequestListStatusFilterComboBox. + /// + public static string PullRequestListStatusFilterComboBox { + get { + return ResourceManager.GetString("PullRequestListStatusFilterComboBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PullRequestListViewCustom. + /// + public static string PullRequestListViewCustom { + get { + return ResourceManager.GetString("PullRequestListViewCustom", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PullRequestRepositoryNameTextBlock. + /// + public static string PullRequestRepositoryNameTextBlock { + get { + return ResourceManager.GetString("PullRequestRepositoryNameTextBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PullRequestSourceBranchHyperlink. + /// + public static string PullRequestSourceBranchHyperlink { + get { + return ResourceManager.GetString("PullRequestSourceBranchHyperlink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PullRequestTargetBranchComboBox. + /// + public static string PullRequestTargetBranchComboBox { + get { + return ResourceManager.GetString("PullRequestTargetBranchComboBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to RepositoryCloneControlCustom. + /// + public static string RepositoryCloneControlCustom { + get { + return ResourceManager.GetString("RepositoryCloneControlCustom", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to RepositoryCreationControlCustom. + /// + public static string RepositoryCreationControlCustom { + get { + return ResourceManager.GetString("RepositoryCreationControlCustom", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to RepositoryDescriptionTextBlock. + /// + public static string RepositoryDescriptionTextBlock { + get { + return ResourceManager.GetString("RepositoryDescriptionTextBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to RepositoryDescriptionTextBox. + /// + public static string RepositoryDescriptionTextBox { + get { + return ResourceManager.GetString("RepositoryDescriptionTextBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to RepositoryGroupItem. + /// + public static string RepositoryGroupItem { + get { + return ResourceManager.GetString("RepositoryGroupItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to RepositoryListBoxItem. + /// + public static string RepositoryListBoxItem { + get { + return ResourceManager.GetString("RepositoryListBoxItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to RepositoryNameTextBlock. + /// + public static string RepositoryNameTextBlock { + get { + return ResourceManager.GetString("RepositoryNameTextBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to RepositoryNameTextBox. + /// + public static string RepositoryNameTextBox { + get { + return ResourceManager.GetString("RepositoryNameTextBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SearchRepositoryTextBox. + /// + public static string SearchRepositoryTextBox { + get { + return ResourceManager.GetString("SearchRepositoryTextBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SelectABranchComboBox. + /// + public static string SelectABranchComboBox { + get { + return ResourceManager.GetString("SelectABranchComboBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SignInCustom. + /// + public static string SignInCustom { + get { + return ResourceManager.GetString("SignInCustom", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SignInDotcomHostTabItem. + /// + public static string SignInDotcomHostTabItem { + get { + return ResourceManager.GetString("SignInDotcomHostTabItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SignInEnterpriseHostTabItem. + /// + public static string SignInEnterpriseHostTabItem { + get { + return ResourceManager.GetString("SignInEnterpriseHostTabItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SignInHostTab. + /// + public static string SignInHostTab { + get { + return ResourceManager.GetString("SignInHostTab", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SignInHyperlink. + /// + public static string SignInHyperlink { + get { + return ResourceManager.GetString("SignInHyperlink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SignInToGitHubTextBlock. + /// + public static string SignInToGitHubTextBlock { + get { + return ResourceManager.GetString("SignInToGitHubTextBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SignOutHyperlink. + /// + public static string SignOutHyperlink { + get { + return ResourceManager.GetString("SignOutHyperlink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TeamExplorerConnectGitHubSectionButton. + /// + public static string TeamExplorerConnectGitHubSectionButton { + get { + return ResourceManager.GetString("TeamExplorerConnectGitHubSectionButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TeamExplorerConnectGitHubSectionTextBlock. + /// + public static string TeamExplorerConnectGitHubSectionTextBlock { + get { + return ResourceManager.GetString("TeamExplorerConnectGitHubSectionTextBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TeamExplorerHomeGitHubSectionButton. + /// + public static string TeamExplorerHomeGitHubSectionButton { + get { + return ResourceManager.GetString("TeamExplorerHomeGitHubSectionButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TeamExplorerHomeGitHubSectionTextBlock. + /// + public static string TeamExplorerHomeGitHubSectionTextBlock { + get { + return ResourceManager.GetString("TeamExplorerHomeGitHubSectionTextBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TeamExplorerPrivateRepositoryCheckBox. + /// + public static string TeamExplorerPrivateRepositoryCheckBox { + get { + return ResourceManager.GetString("TeamExplorerPrivateRepositoryCheckBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TeamExplorerPublishAccountComboBox. + /// + public static string TeamExplorerPublishAccountComboBox { + get { + return ResourceManager.GetString("TeamExplorerPublishAccountComboBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TeamExplorerPublishHostComboBox. + /// + public static string TeamExplorerPublishHostComboBox { + get { + return ResourceManager.GetString("TeamExplorerPublishHostComboBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TeamExplorerPublishRepositoryButton. + /// + public static string TeamExplorerPublishRepositoryButton { + get { + return ResourceManager.GetString("TeamExplorerPublishRepositoryButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TeamExplorerPublishRepositoryDescriptionTextBox. + /// + public static string TeamExplorerPublishRepositoryDescriptionTextBox { + get { + return ResourceManager.GetString("TeamExplorerPublishRepositoryDescriptionTextBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TeamExplorerPublishRepositoryNameTextBox. + /// + public static string TeamExplorerPublishRepositoryNameTextBox { + get { + return ResourceManager.GetString("TeamExplorerPublishRepositoryNameTextBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TeamExplorerRepositoryListBoxItem. + /// + public static string TeamExplorerRepositoryListBoxItem { + get { + return ResourceManager.GetString("TeamExplorerRepositoryListBoxItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TeamExplorerRepositoryListView. + /// + public static string TeamExplorerRepositoryListView { + get { + return ResourceManager.GetString("TeamExplorerRepositoryListView", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TeamExplorerRepositoryNameTextBlock. + /// + public static string TeamExplorerRepositoryNameTextBlock { + get { + return ResourceManager.GetString("TeamExplorerRepositoryNameTextBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TeamExplorerRepositoryURLTextBlock. + /// + public static string TeamExplorerRepositoryURLTextBlock { + get { + return ResourceManager.GetString("TeamExplorerRepositoryURLTextBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TeamExplorerSyncGitHubRepositoryPublishCustom. + /// + public static string TeamExplorerSyncGitHubRepositoryPublishCustom { + get { + return ResourceManager.GetString("TeamExplorerSyncGitHubRepositoryPublishCustom", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TeamExplorerSyncGitHubSectionButton. + /// + public static string TeamExplorerSyncGitHubSectionButton { + get { + return ResourceManager.GetString("TeamExplorerSyncGitHubSectionButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TeamExplorerSyncGitHubSectionTextBlock. + /// + public static string TeamExplorerSyncGitHubSectionTextBlock { + get { + return ResourceManager.GetString("TeamExplorerSyncGitHubSectionTextBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TwoFactorAuthenticationCloseButton. + /// + public static string TwoFactorAuthenticationCloseButton { + get { + return ResourceManager.GetString("TwoFactorAuthenticationCloseButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TwoFactorAuthenticationCustom. + /// + public static string TwoFactorAuthenticationCustom { + get { + return ResourceManager.GetString("TwoFactorAuthenticationCustom", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TwoFactorAuthenticationInformationTextBlock. + /// + public static string TwoFactorAuthenticationInformationTextBlock { + get { + return ResourceManager.GetString("TwoFactorAuthenticationInformationTextBlock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TwoFactorAuthenticationInputStackPanel. + /// + public static string TwoFactorAuthenticationInputStackPanel { + get { + return ResourceManager.GetString("TwoFactorAuthenticationInputStackPanel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TwoFactorAuthenticationLearnMoreHyperlink. + /// + public static string TwoFactorAuthenticationLearnMoreHyperlink { + get { + return ResourceManager.GetString("TwoFactorAuthenticationLearnMoreHyperlink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TwoFactorAuthenticationTitleBar. + /// + public static string TwoFactorAuthenticationTitleBar { + get { + return ResourceManager.GetString("TwoFactorAuthenticationTitleBar", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TwoFactorAuthenticationWindow. + /// + public static string TwoFactorAuthenticationWindow { + get { + return ResourceManager.GetString("TwoFactorAuthenticationWindow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TwoFactorAuthenticatonInputCustom. + /// + public static string TwoFactorAuthenticatonInputCustom { + get { + return ResourceManager.GetString("TwoFactorAuthenticatonInputCustom", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TwoFactorAuthenticatonVerifyButton. + /// + public static string TwoFactorAuthenticatonVerifyButton { + get { + return ResourceManager.GetString("TwoFactorAuthenticatonVerifyButton", resourceCulture); + } + } + } +} diff --git a/src/GitHub.UI/TestAutomation/AutomationIDs.resx b/src/GitHub.UI/TestAutomation/AutomationIDs.resx new file mode 100644 index 0000000000..0ba6b768fc --- /dev/null +++ b/src/GitHub.UI/TestAutomation/AutomationIDs.resx @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + CreateAGitHubRepositoryWindow + + + GitHubInfoPanel + + + GitHubTabItem + + + PullRequestCreationViewCustom + + + PullRequestCreationDetailPane + + + PullRequestListViewCustom + + + PullRequestCreationCancelButton + + + PullRequestCreationCreateButton + + + PullRequestTargetBranchComboBox + + + PullRequestCreationDescriptionTextBox + + + PullRequestCreationTitleTextBox + + + PullRequestListFilterAssigneesDropDown + + + PullRequestSourceBranchHyperlink + + + CreatedPullRequestAuthorImage + + + CreatedPullRequestDetailsTextBlock + + + CreatedPullRequestListItem + + + CreatedPullRequestNumberHyperlink + + + CreatedPullRequestTitleHyperlink + + + CreateNewHyperlink + + + PullRequestListAssigneeFilterComboBox + + + PullRequestListAuthorFilterComboBox + + + PullRequestListStatusFilterComboBox + + + SelectABranchComboBox + + + PullRequestRepositoryNameTextBlock + + + AccountComboBox + + + AccountListBoxItem + + + CloneAGitHubRepositoryWindow + + + CloneRepositoryButton + + + CloneRepositoryCloseButton + + + CloneRepositoryLocalPathBrowsePathButton + + + CloneRepositoryLocalPathCustom + + + CloneRepositoryTitleBar + + + ConnectToGitHubCloseButton + + + ConnectToGitHubTitleBar + + + ConnectToGitHubWindow + + + CreateAGitHubGistControl + + + CreateAGitHubGistTitleBar + + + CreateGistButton + + + CreateRepositoryButton + + + CreateRepositoryCloseButton + + + CreateRepositoryLocalPathBrowseButton + + + CreateRepositoryLocalPathTextBox + + + CreateRepositoryTitleBar + + + DontHaveDotcomAccountTextBlock + + + DontHaveEnterpriseTextBlock + + + DotcomPasswordTextBox + + + DotcomSignInButton + + + DotcomSignUpHyperlink + + + DotcomUsernameEmailTextBox + + + EnterpriseLearnMoreHyperlink + + + EnterprisePasswordTextBox + + + EnterpriseServerAddressTextBox + + + EnterpriseSignInButton + + + EnterpriseUsernameEmailTextBox + + + GistAccountImage + + + GistAccountNameTextBlock + + + GistCreationControlCustom + + + GistDescriptionTextBlock + + + GistDescriptionTextBox + + + GistErrorMessageTextBlock + + + GistFileNameTextBlock + + + GistFileNameTextBox + + + GitignoreComboBox + + + GitignoreFilterTextBox + + + GitignoreListBoxItem + + + GitignorePopupWindow + + + GitignoreTextBlock + + + LicenseComboBox + + + LicenseFilterTextBox + + + LicenseListBoxItem + + + LicensePopupWindow + + + LicenseTextBlock + + + PrivateGistCheckBox + + + PrivateRepositoryCheckBox + + + RepositoryCloneControlCustom + + + RepositoryCreationControlCustom + + + RepositoryDescriptionTextBlock + + + RepositoryDescriptionTextBox + + + RepositoryGroupItem + + + RepositoryListBoxItem + + + RepositoryNameTextBlock + + + RepositoryNameTextBox + + + SearchRepositoryTextBox + + + SignInCustom + + + SignInDotcomHostTabItem + + + SignInEnterpriseHostTabItem + + + SignInHostTab + + + TwoFactorAuthenticationCloseButton + + + TwoFactorAuthenticationCustom + + + TwoFactorAuthenticationInformationTextBlock + + + TwoFactorAuthenticationLearnMoreHyperlink + + + TwoFactorAuthenticationTitleBar + + + TwoFactorAuthenticationWindow + + + TwoFactorAuthenticatonInputCustom + + + TwoFactorAuthenticatonVerifyButton + + + CloneHyperlink + + + ConnectToGitHubMenuItem + + + CreateHyperlink + + + GitHubConnectContentCustom + + + GitHubHomeContentCustom + + + SignInHyperlink + + + SignOutHyperlink + + + TeamExplorerConnectGitHubSectionButton + + + TeamExplorerConnectGitHubSectionTextBlock + + + TeamExplorerHomeGitHubSectionButton + + + TeamExplorerHomeGitHubSectionTextBlock + + + TeamExplorerRepositoryListBoxItem + + + TeamExplorerRepositoryListView + + + TeamExplorerRepositoryNameTextBlock + + + TeamExplorerRepositoryURLTextBlock + + + TeamExplorerPrivateRepositoryCheckBox + + + TeamExplorerPublishAccountComboBox + + + TeamExplorerPublishHostComboBox + + + TeamExplorerPublishRepositoryButton + + + TeamExplorerPublishRepositoryDescriptionTextBox + + + TeamExplorerPublishRepositoryNameTextBox + + + TeamExplorerSyncGitHubRepositoryPublishCustom + + + TeamExplorerSyncGitHubSectionButton + + + TeamExplorerSyncGitHubSectionTextBlock + + + GitHubInfoPanelCancelButton + + + GitHubInfoPanelMessageDocument + + + GitHubToolBar + + + GitHubToolBarBackImage + + + GitHubToolBarForwardImage + + + GitHubToolBarPullRequestImage + + + GitHubToolBarRefreshImage + + + GitHubLoggedOutCreateAnAccountHyperlink + + + GitHubLoggedOutSignInHyperlink + + + LoggedOutViewCustom + + + PowerfulCollaborationTextBlock + + + SignInToGitHubTextBlock + + + GitHubPaneView + + + TwoFactorAuthenticationInputStackPanel + + \ No newline at end of file diff --git a/src/GitHub.UI/Themes/Generic.xaml b/src/GitHub.UI/Themes/Generic.xaml new file mode 100644 index 0000000000..41baf6c6ff --- /dev/null +++ b/src/GitHub.UI/Themes/Generic.xaml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/src/GitHub.UI/packages.config b/src/GitHub.UI/packages.config index 2a53434606..ea78bb8317 100644 --- a/src/GitHub.UI/packages.config +++ b/src/GitHub.UI/packages.config @@ -1,5 +1,7 @@  - - + + + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio.UI/Base/TeamExplorerBase.cs b/src/GitHub.VisualStudio.UI/Base/TeamExplorerBase.cs new file mode 100644 index 0000000000..7f022105c5 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Base/TeamExplorerBase.cs @@ -0,0 +1,57 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using GitHub.Primitives; +using GitHub.Services; +using GitHub.Extensions; + +namespace GitHub.VisualStudio.Base +{ + public abstract class TeamExplorerBase : NotificationAwareObject, IDisposable + { + public static readonly Guid TeamExplorerConnectionsSectionId = new Guid("ef6a7a99-f01f-4c91-ad31-183c1354dd97"); + + protected IServiceProvider TEServiceProvider + { + get; set; + } + + protected IGitHubServiceProvider ServiceProvider + { + get; + } + + protected TeamExplorerBase(IGitHubServiceProvider serviceProvider) + { + Guard.ArgumentNotNull(serviceProvider, nameof(serviceProvider)); + + ServiceProvider = serviceProvider; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + } + + protected static void OpenInBrowser(Lazy browser, Uri uri) + { + Guard.ArgumentNotNull(browser, nameof(browser)); + Guard.ArgumentNotNull(uri, nameof(uri)); + + OpenInBrowser(browser.Value, uri); + } + + protected static void OpenInBrowser(IVisualStudioBrowser browser, Uri uri) + { + Guard.ArgumentNotNull(browser, nameof(browser)); + Guard.ArgumentNotNull(uri, nameof(uri)); + + browser?.OpenUrl(uri); + } + } +} diff --git a/src/GitHub.VisualStudio/Base/TeamExplorerGitRepoInfo.cs b/src/GitHub.VisualStudio.UI/Base/TeamExplorerGitRepoInfo.cs similarity index 76% rename from src/GitHub.VisualStudio/Base/TeamExplorerGitRepoInfo.cs rename to src/GitHub.VisualStudio.UI/Base/TeamExplorerGitRepoInfo.cs index 176028ca14..f8e0ca43ed 100644 --- a/src/GitHub.VisualStudio/Base/TeamExplorerGitRepoInfo.cs +++ b/src/GitHub.VisualStudio.UI/Base/TeamExplorerGitRepoInfo.cs @@ -2,25 +2,21 @@ using GitHub.Primitives; using GitHub.Services; using GitHub.VisualStudio.Helpers; -using Microsoft.VisualStudio.TeamFoundation.Git.Extensibility; -using NullGuard; namespace GitHub.VisualStudio.Base { public class TeamExplorerGitRepoInfo : TeamExplorerBase, IGitAwareItem { - public TeamExplorerGitRepoInfo() + public TeamExplorerGitRepoInfo(IGitHubServiceProvider serviceProvider) : base(serviceProvider) { activeRepo = null; activeRepoUri = null; activeRepoName = string.Empty; } - ISimpleRepositoryModel activeRepo; - [AllowNull] - public ISimpleRepositoryModel ActiveRepo + ILocalRepositoryModel activeRepo; + public ILocalRepositoryModel ActiveRepo { - [return: AllowNull] get { return activeRepo; } set { @@ -35,10 +31,9 @@ public ISimpleRepositoryModel ActiveRepo /// /// Represents the web URL of the repository on GitHub.com, even if the origin is an SSH address. /// - [AllowNull] public UriString ActiveRepoUri { - [return: AllowNull] get { return activeRepoUri; } + get { return activeRepoUri; } set { activeRepoUri = value; this.RaisePropertyChange(); } } diff --git a/src/GitHub.VisualStudio.UI/Base/TeamExplorerItemBase.cs b/src/GitHub.VisualStudio.UI/Base/TeamExplorerItemBase.cs new file mode 100644 index 0000000000..917a0b7fee --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Base/TeamExplorerItemBase.cs @@ -0,0 +1,203 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using GitHub.Api; +using GitHub.Models; +using GitHub.Primitives; +using GitHub.Services; +using GitHub.VisualStudio.Helpers; +using GitHub.ViewModels; +using GitHub.VisualStudio.UI; +using GitHub.Extensions; + +namespace GitHub.VisualStudio.Base +{ + public class TeamExplorerItemBase : TeamExplorerGitRepoInfo, IServiceProviderAware + { + readonly ISimpleApiClientFactory apiFactory; + protected ITeamExplorerServiceHolder holder; + + ISimpleApiClient simpleApiClient; + public ISimpleApiClient SimpleApiClient + { + get { return simpleApiClient; } + set + { + if (simpleApiClient != value && value == null) + apiFactory.ClearFromCache(simpleApiClient); + simpleApiClient = value; + } + } + + protected ISimpleApiClientFactory ApiFactory => apiFactory; + + public TeamExplorerItemBase(IGitHubServiceProvider serviceProvider, ITeamExplorerServiceHolder holder) + : base(serviceProvider) + { + Guard.ArgumentNotNull(serviceProvider, nameof(serviceProvider)); + Guard.ArgumentNotNull(holder, nameof(holder)); + + this.holder = holder; + } + + public TeamExplorerItemBase(IGitHubServiceProvider serviceProvider, + ISimpleApiClientFactory apiFactory, ITeamExplorerServiceHolder holder) + : base(serviceProvider) + { + Guard.ArgumentNotNull(serviceProvider, nameof(serviceProvider)); + Guard.ArgumentNotNull(apiFactory, nameof(apiFactory)); + Guard.ArgumentNotNull(holder, nameof(holder)); + + this.apiFactory = apiFactory; + this.holder = holder; + } + + public virtual void Initialize(IServiceProvider serviceProvider) + { + Guard.ArgumentNotNull(serviceProvider, nameof(serviceProvider)); + + TEServiceProvider = serviceProvider; + Debug.Assert(holder != null, "Could not get an instance of TeamExplorerServiceHolder"); + if (holder == null) + return; + holder.ServiceProvider = TEServiceProvider; + SubscribeToRepoChanges(); + } + + + public virtual void Execute() + { + } + + public virtual void Invalidate() + { + } + + void SubscribeToRepoChanges() + { + holder.Subscribe(this, (ILocalRepositoryModel repo) => + { + var changed = !Equals(ActiveRepo, repo); + ActiveRepo = repo; + RepoChanged(changed); + }); + } + + void Unsubscribe() + { + holder.Unsubscribe(this); + if (TEServiceProvider != null) + holder.ClearServiceProvider(TEServiceProvider); + } + + protected virtual void RepoChanged(bool changed) + { + var repo = ActiveRepo; + if (repo != null) + { + var uri = repo.CloneUrl; + if (uri?.RepositoryName != null) + { + ActiveRepoUri = uri; + ActiveRepoName = uri.NameWithOwner; + } + } + } + + protected async Task GetRepositoryOrigin() + { + if (ActiveRepo == null) + return RepositoryOrigin.NonGitRepository; + + var uri = ActiveRepoUri; + if (uri == null) + return RepositoryOrigin.Other; + + Debug.Assert(apiFactory != null, "apiFactory cannot be null. Did you call the right constructor?"); + SimpleApiClient = await apiFactory.Create(uri); + + var isdotcom = HostAddress.IsGitHubDotComUri(uri.ToRepositoryUrl()); + + if (isdotcom) + { + return RepositoryOrigin.DotCom; + } + else + { + var repo = await SimpleApiClient.GetRepository(); + + if ((repo.FullName == ActiveRepoName || repo.Id == 0) && await SimpleApiClient.IsEnterprise()) + { + return RepositoryOrigin.Enterprise; + } + } + + return RepositoryOrigin.Other; + } + + protected async Task IsAGitHubRepo() + { + var origin = await GetRepositoryOrigin(); + return origin == RepositoryOrigin.DotCom || origin == RepositoryOrigin.Enterprise; + } + + protected async Task IsAGitHubDotComRepo() + { + var origin = await GetRepositoryOrigin(); + return origin == RepositoryOrigin.DotCom; + } + + protected async Task IsUserAuthenticated() + { + if (SimpleApiClient == null) + { + if (ActiveRepo == null) + return false; + + var uri = ActiveRepoUri; + if (uri == null) + return false; + + SimpleApiClient = await apiFactory.Create(uri); + } + + return SimpleApiClient?.IsAuthenticated() ?? false; + } + + bool disposed; + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (!disposed) + { + Unsubscribe(); + disposed = true; + } + } + base.Dispose(disposing); + } + + bool isEnabled; + public bool IsEnabled + { + get { return isEnabled; } + set { isEnabled = value; this.RaisePropertyChange(); } + } + + bool isVisible; + public bool IsVisible + { + get { return isVisible; } + set { isVisible = value; this.RaisePropertyChange(); } + } + + string text; + public string Text + { + get { return text; } + set { text = value; this.RaisePropertyChange(); } + } + + } +} \ No newline at end of file diff --git a/src/GitHub.VisualStudio.UI/Colors.cs b/src/GitHub.VisualStudio.UI/Colors.cs new file mode 100644 index 0000000000..f9966c2d5d --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Colors.cs @@ -0,0 +1,58 @@ +using Microsoft.VisualStudio.PlatformUI; +using System; +using System.Windows.Media; + +namespace GitHub.VisualStudio.Helpers +{ + public static class Colors + { + public static Color RedNavigationItem = Color.FromRgb(0xF0, 0x50, 0x33); + public static Color BlueNavigationItem = Color.FromRgb(0x00, 0x79, 0xCE); + public static Color LightBlueNavigationItem = Color.FromRgb(0x00, 0x9E, 0xCE); + public static Color DarkPurpleNavigationItem = Color.FromRgb(0x68, 0x21, 0x7A); + public static Color GrayNavigationItem = Color.FromRgb(0x73, 0x82, 0x8C); + public static Color YellowNavigationItem = Color.FromRgb(0xF9, 0xC9, 0x00); + public static Color PurpleNavigationItem = Color.FromRgb(0xAE, 0x3C, 0xBA); + + public static Color LightThemeNavigationItem = Color.FromRgb(66, 66, 66); + public static Color DarkThemeNavigationItem = Color.FromRgb(200, 200, 200); + + public static int ToInt32(this Color color) + { + return BitConverter.ToInt32(new byte[]{ color.B, color.G, color.R, color.A }, 0); + } + + public static Color ToColor(this System.Drawing.Color color) + { + return Color.FromArgb(color.A, color.R, color.G, color.B); + } + + + static Color AccentMediumDarkTheme = Color.FromRgb(45, 45, 48); + static Color AccentMediumLightTheme = Color.FromRgb(238, 238, 242); + static Color AccentMediumBlueTheme = Color.FromRgb(255, 236, 181); + + public static string DetectTheme() + { + try + { + var color = VSColorTheme.GetThemedColor(EnvironmentColors.AccentMediumColorKey); + var cc = color.ToColor(); + if (cc == AccentMediumBlueTheme) + return "Blue"; + if (cc == AccentMediumLightTheme) + return "Light"; + if (cc == AccentMediumDarkTheme) + return "Dark"; + var brightness = color.GetBrightness(); + var dark = brightness < 0.5f; + return dark ? "Dark" : "Light"; + } + // this throws in design time and when running outside of VS + catch (ArgumentNullException) + { + return "Dark"; + } + } + } +} diff --git a/src/GitHub.VisualStudio.UI/Constants.cs b/src/GitHub.VisualStudio.UI/Constants.cs new file mode 100644 index 0000000000..0378a5e916 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Constants.cs @@ -0,0 +1,15 @@ +namespace GitHub.VisualStudio.UI +{ + public static class Constants + { + public const string NoAngleBracketsErrorMessage = "Failed to parse signature - Neither `name` nor `email` should contain angle brackets chars."; + public const int MaxRepositoryNameLength = 100; + public const int MaxDirectoryLength = 200; // Windows allows 248, but we need to allow room for subdirectories. + public const int MaxFilePathLength = 260; + + public const string Notification_RepoCreated = "[{0}](u:{1}) has been successfully created."; + public const string Notification_RepoCloned = "[{0}](u:{1}) has been successfully cloned."; + public const string Notification_CreateNewProject = "[Create a new project or solution](c:{0})."; + public const string Notification_OpenProject = "[Open an existing project or solution](o:{0})."; + } +} diff --git a/src/GitHub.VisualStudio.UI/GitHub.VisualStudio.UI.csproj b/src/GitHub.VisualStudio.UI/GitHub.VisualStudio.UI.csproj new file mode 100644 index 0000000000..3e15e52e4e --- /dev/null +++ b/src/GitHub.VisualStudio.UI/GitHub.VisualStudio.UI.csproj @@ -0,0 +1,275 @@ + + + + + Debug + AnyCPU + {D1DFBB0C-B570-4302-8F1E-2E3A19C41961} + Library + Properties + GitHub.VisualStudio.UI + GitHub.VisualStudio.UI + 7.3 + v4.6.1 + 512 + ..\common\GitHubVS.ruleset + true + true + + + true + full + false + DEBUG;TRACE + prompt + 4 + false + bin\Debug\ + + + true + full + false + CODE_ANALYSIS;DEBUG;TRACE + prompt + 4 + true + bin\Debug\ + + + pdbonly + true + TRACE + prompt + 4 + true + bin\Release\ + + + + + ..\..\packages\Markdig.Signed.0.13.0\lib\net40\Markdig.dll + True + + + ..\..\packages\Markdig.Wpf.Signed.0.2.1\lib\net452\Markdig.Wpf.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Shell.14.0.14.3.25407\lib\Microsoft.VisualStudio.Shell.14.0.dll + True + + + ..\..\packages\Microsoft.VisualStudio.Shell.Immutable.11.0.11.0.50727\lib\net45\Microsoft.VisualStudio.Shell.Immutable.11.0.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + Resources.resx + True + True + + + + + AccountAvatar.xaml + + + InfoPanel.xaml + + + + GitHubConnectContent.xaml + + + GitHubHomeContent.xaml + + + GitHubInvitationContent.xaml + + + Properties\SolutionInfo.cs + + + + + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + + + {08dd4305-7787-4823-a53f-4d0f725a07f3} + Octokit + + + {1CE2D235-8072-4649-BA5A-CFB1AF8776E0} + ReactiveUI_Net45 + + + {1a1da411-8d1f-4578-80a6-04576bea2dc5} + GitHub.App + + + {9AEA02DB-02B5-409C-B0CA-115D05331A6B} + GitHub.Exports + + + {6afe2e2d-6db0-4430-a2ea-f5f5388d2f78} + GitHub.Extensions + + + {8d73575a-a89f-47cc-b153-b47dd06837f0} + GitHub.Logging + + + {346384dd-2445-4a28-af22-b45f3957bd89} + GitHub.UI + + + + + PublicResXFileCodeGenerator + Resources.Designer.cs + Designer + + + + + + + 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}. + + + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio.UI/Helpers/ThemeDictionaryManager.cs b/src/GitHub.VisualStudio.UI/Helpers/ThemeDictionaryManager.cs new file mode 100644 index 0000000000..4ff54b5818 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Helpers/ThemeDictionaryManager.cs @@ -0,0 +1,81 @@ +using System; +using System.Windows; +using System.ComponentModel; +using Microsoft.VisualStudio.PlatformUI; +using GitHub.VisualStudio.Helpers; +using GitHub.UI.Helpers; + +namespace GitHub.VisualStudio.UI.Helpers +{ + public class ThemeDictionaryManager : SharedDictionaryManager, IDisposable + { + static bool isInDesignMode; + Uri baseThemeUri; + + static ThemeDictionaryManager() + { + isInDesignMode = DesignerProperties.GetIsInDesignMode(new DependencyObject()); + } + + public override Uri Source + { + get { return base.Source; } + set + { + InitTheme(value); + base.Source = GetCurrentThemeUri(); + } + } + + void InitTheme(Uri themeUri) + { + if (baseThemeUri == null) + { + baseThemeUri = themeUri; + if (!isInDesignMode) + { + VSColorTheme.ThemeChanged += OnThemeChange; + } + } + } + + void OnThemeChange(ThemeChangedEventArgs e) + { + base.Source = GetCurrentThemeUri(); + } + + Uri GetCurrentThemeUri() + { + if (isInDesignMode) + { + return baseThemeUri; + } + + var currentTheme = Colors.DetectTheme(); + return new Uri(baseThemeUri, "Theme" + currentTheme + ".xaml"); + } + + bool disposed; + private void Dispose(bool disposing) + { + if (disposed) return; + if (disposing) + { + disposed = true; + if (baseThemeUri != null) + { + baseThemeUri = null; + if (!isInDesignMode) + { + VSColorTheme.ThemeChanged -= OnThemeChange; + } + } + } + } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/GitHub.VisualStudio.UI/Properties/AssemblyInfo.cs b/src/GitHub.VisualStudio.UI/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..0d5801ea70 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Properties/AssemblyInfo.cs @@ -0,0 +1,12 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows.Markup; + +[assembly: AssemblyTitle("GitHub.VisualStudio.UI")] +[assembly: AssemblyDescription("GitHub.VisualStudio.UI")] +[assembly: Guid("d1dfbb0c-b570-4302-8f1e-2e3a19c41961")] + +[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.VisualStudio.UI")] +[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.VisualStudio.UI.Controls")] +[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.VisualStudio.UI.Views")] diff --git a/src/GitHub.VisualStudio.UI/RepositoryOrigin.cs b/src/GitHub.VisualStudio.UI/RepositoryOrigin.cs new file mode 100644 index 0000000000..0cec4559a9 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/RepositoryOrigin.cs @@ -0,0 +1,11 @@ +namespace GitHub.VisualStudio.UI +{ + public enum RepositoryOrigin + { + Unknown, + DotCom, + Enterprise, + Other, + NonGitRepository, + } +} diff --git a/src/GitHub.VisualStudio.UI/Resources.Designer.cs b/src/GitHub.VisualStudio.UI/Resources.Designer.cs new file mode 100644 index 0000000000..c497412cab --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Resources.Designer.cs @@ -0,0 +1,1046 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace GitHub.VisualStudio.UI { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GitHub.VisualStudio.UI.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Add review comment. + /// + public static string AddReviewComment { + get { + return ResourceManager.GetString("AddReviewComment", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add a single comment. + /// + public static string AddSingleComment { + get { + return ResourceManager.GetString("AddSingleComment", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add your review. + /// + public static string AddYourReview { + get { + return ResourceManager.GetString("AddYourReview", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid authentication code. + /// + public static string authenticationFailedLabelContent { + get { + return ResourceManager.GetString("authenticationFailedLabelContent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Try entering the code again or clicking the resend button to get a new authentication code.. + /// + public static string authenticationFailedLabelMessage { + get { + return ResourceManager.GetString("authenticationFailedLabelMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Authentication code sent!. + /// + public static string authenticationSentLabelContent { + get { + return ResourceManager.GetString("authenticationSentLabelContent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If you do not receive the authentication code, contact support@github.com.. + /// + public static string authenticationSentLabelMessage { + get { + return ResourceManager.GetString("authenticationSentLabelMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The GitHub extension is not available inside Blend. + /// + public static string BlendDialogText { + get { + return ResourceManager.GetString("BlendDialogText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Powerful collaboration, code review, and code management for open source and private projects.. + /// + public static string BlurbText { + get { + return ResourceManager.GetString("BlurbText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Browse. + /// + public static string browsePathButtonContent { + get { + return ResourceManager.GetString("browsePathButtonContent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cancel. + /// + public static string CancelLink { + get { + return ResourceManager.GetString("CancelLink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Changes ({0}). + /// + public static string ChangesCountFormat { + get { + return ResourceManager.GetString("ChangesCountFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Clone. + /// + public static string CloneLink { + get { + return ResourceManager.GetString("CloneLink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Compare File as Default Action. + /// + public static string CompareFileAsDefaultAction { + get { + return ResourceManager.GetString("CompareFileAsDefaultAction", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Continue your review. + /// + public static string ContinueYourReview { + get { + return ResourceManager.GetString("ContinueYourReview", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not connect to github.com. + /// + public static string couldNotConnectToGitHubText { + get { + return ResourceManager.GetString("couldNotConnectToGitHubText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not connect to the server.. + /// + public static string couldNotConnectToTheServerText { + get { + return ResourceManager.GetString("couldNotConnectToTheServerText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Create an account. + /// + public static string CreateAccountLink { + get { + return ResourceManager.GetString("CreateAccountLink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Create. + /// + public static string CreateLink { + get { + return ResourceManager.GetString("CreateLink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Are you sure you want to delete this comment?. + /// + public static string DeleteCommentConfirmation { + get { + return ResourceManager.GetString("DeleteCommentConfirmation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delete Comment. + /// + public static string DeleteCommentConfirmationCaption { + get { + return ResourceManager.GetString("DeleteCommentConfirmationCaption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Description. + /// + public static string Description { + get { + return ResourceManager.GetString("Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Description (Optional). + /// + public static string DescriptionOptional { + get { + return ResourceManager.GetString("DescriptionOptional", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Don’t have an account? . + /// + public static string dontHaveAnAccountText { + get { + return ResourceManager.GetString("dontHaveAnAccountText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Don’t have GitHub Enterprise? . + /// + public static string dontHaveGitHubEnterpriseText { + get { + return ResourceManager.GetString("dontHaveGitHubEnterpriseText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please check your internet connection and try again.. + /// + public static string dotComConnectionFailedMessageMessage { + get { + return ResourceManager.GetString("dotComConnectionFailedMessageMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The host isn't available or is not a GitHub Enterprise server. Check the address and try again.. + /// + public static string enterpriseConnectingFailedMessage { + get { + return ResourceManager.GetString("enterpriseConnectingFailedMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GitHub Enterprise server address. + /// + public static string enterpriseUrlPromptText { + get { + return ResourceManager.GetString("enterpriseUrlPromptText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not copy to the clipboard. Please try again.. + /// + public static string Error_FailedToCopyToClipboard { + get { + return ResourceManager.GetString("Error_FailedToCopyToClipboard", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to File Name. + /// + public static string fileNameText { + get { + return ResourceManager.GetString("fileNameText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Filter branches. + /// + public static string filterBranchesText { + get { + return ResourceManager.GetString("filterBranchesText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Search repositories. + /// + public static string filterTextPromptText { + get { + return ResourceManager.GetString("filterTextPromptText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to (forgot your password?). + /// + public static string ForgotPasswordLink { + get { + return ResourceManager.GetString("ForgotPasswordLink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fork. + /// + public static string ForkNavigationItemText { + get { + return ResourceManager.GetString("ForkNavigationItemText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Get Started. + /// + public static string GetStartedText { + get { + return ResourceManager.GetString("GetStartedText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gist created. + /// + public static string gistCreatedMessage { + get { + return ResourceManager.GetString("gistCreatedMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to create gist. + /// + public static string gistCreationFailedMessage { + get { + return ResourceManager.GetString("gistCreationFailedMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connect…. + /// + public static string GitHubInvitationSectionConnectLabel { + get { + return ResourceManager.GetString("GitHubInvitationSectionConnectLabel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Publish to GitHub. + /// + public static string GitHubPublishSectionTitle { + get { + return ResourceManager.GetString("GitHubPublishSectionTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Graphs. + /// + public static string GraphsNavigationItemText { + get { + return ResourceManager.GetString("GraphsNavigationItemText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Git ignore. + /// + public static string ignoreTemplateListText { + get { + return ResourceManager.GetString("ignoreTemplateListText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Issues. + /// + public static string IssuesNavigationItemText { + get { + return ResourceManager.GetString("IssuesNavigationItemText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Learn more. + /// + public static string learnMoreLink { + get { + return ResourceManager.GetString("learnMoreLink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to License. + /// + public static string licenseListText { + get { + return ResourceManager.GetString("licenseListText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Link copied to clipboard. + /// + public static string LinkCopiedToClipboardMessage { + get { + return ResourceManager.GetString("LinkCopiedToClipboardMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Some or all repositories may not have loaded. Close the dialog and try again.. + /// + public static string loadingFailedMessageContent { + get { + return ResourceManager.GetString("loadingFailedMessageContent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An error occurred while loading repositories. + /// + public static string loadingFailedMessageMessage { + get { + return ResourceManager.GetString("loadingFailedMessageMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Local branch up to date. + /// + public static string LocalBranchUpToDate { + get { + return ResourceManager.GetString("LocalBranchUpToDate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Local path. + /// + public static string localPathText { + get { + return ResourceManager.GetString("localPathText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Check your username and password, then try again. + /// + public static string LoginFailedMessage { + get { + return ResourceManager.GetString("LoginFailedMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sign in failed.. + /// + public static string LoginFailedText { + get { + return ResourceManager.GetString("LoginFailedText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sign in. + /// + public static string LoginLink { + get { + return ResourceManager.GetString("LoginLink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Private Repository. + /// + public static string makePrivateContent { + get { + return ResourceManager.GetString("makePrivateContent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Private Gist. + /// + public static string makePrivateGist { + get { + return ResourceManager.GetString("makePrivateGist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Name. + /// + public static string nameText { + get { + return ResourceManager.GetString("nameText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No repositories. + /// + public static string noRepositoriesMessageText { + get { + return ResourceManager.GetString("noRepositoriesMessageText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This repository is not on GitHub. + /// + public static string NotAGitHubRepository { + get { + return ResourceManager.GetString("NotAGitHubRepository", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Publish this repository to GitHub and get powerful collaboration, code review, and code management for open source and private projects.. + /// + public static string NotAGitHubRepositoryMessage { + get { + return ResourceManager.GetString("NotAGitHubRepositoryMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No repository. + /// + public static string NotAGitRepository { + get { + return ResourceManager.GetString("NotAGitRepository", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to We couldn't find a git repository here. Open a git project or click "File -> Add to Source Control" in a project to get started.. + /// + public static string NotAGitRepositoryMessage { + get { + return ResourceManager.GetString("NotAGitRepositoryMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You are not signed in to {0}, so certain git operations may fail. [Sign in now]({1}). + /// + public static string NotLoggedInMessage { + get { + return ResourceManager.GetString("NotLoggedInMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open. + /// + public static string Open { + get { + return ResourceManager.GetString("Open", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open File as Default Action. + /// + public static string OpenFileAsDefaultAction { + get { + return ResourceManager.GetString("OpenFileAsDefaultAction", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open File in Solution. + /// + public static string OpenFileInSolution { + get { + return ResourceManager.GetString("OpenFileInSolution", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open in Browser. + /// + public static string openInBrowser { + get { + return ResourceManager.GetString("openInBrowser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to View Pull Request on GitHub. + /// + public static string OpenPROnGitHubToolTip { + get { + return ResourceManager.GetString("OpenPROnGitHubToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open the two-factor authentication app on your device to view your authentication code.. + /// + public static string openTwoFactorAuthAppText { + get { + return ResourceManager.GetString("openTwoFactorAuthAppText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Debugging. + /// + public static string Options_DebuggingTitle { + get { + return ResourceManager.GetString("Options_DebuggingTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show PR comments on editor margin. + /// + public static string Options_EditorCommentsLabel { + get { + return ResourceManager.GetString("Options_EditorCommentsLabel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enable Trace Logging. + /// + public static string Options_EnableTraceLoggingText { + get { + return ResourceManager.GetString("Options_EnableTraceLoggingText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to These features might change in a future version. + /// + public static string Options_ExperimentalNote { + get { + return ResourceManager.GetString("Options_ExperimentalNote", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Experimental features. + /// + public static string Options_ExperimentalTitle { + get { + return ResourceManager.GetString("Options_ExperimentalTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show Fork button in Team Explorer. + /// + public static string Options_ForkButtonLabel { + get { + return ResourceManager.GetString("Options_ForkButtonLabel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Help us improve by sending anonymous usage data. + /// + public static string Options_MetricsLabel { + get { + return ResourceManager.GetString("Options_MetricsLabel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Privacy. + /// + public static string Options_PrivacyTitle { + get { + return ResourceManager.GetString("Options_PrivacyTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to or. + /// + public static string orText { + get { + return ResourceManager.GetString("orText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password. + /// + public static string PasswordPrompt { + get { + return ResourceManager.GetString("PasswordPrompt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Path. + /// + public static string pathText { + get { + return ResourceManager.GetString("pathText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to by. + /// + public static string prUpdatedByText { + get { + return ResourceManager.GetString("prUpdatedByText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Updated. + /// + public static string prUpdatedText { + get { + return ResourceManager.GetString("prUpdatedText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Publish. + /// + public static string publishText { + get { + return ResourceManager.GetString("publishText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Publish to GitHub. + /// + public static string PublishToGitHubButton { + get { + return ResourceManager.GetString("PublishToGitHubButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pull Requests. + /// + public static string PullRequestsNavigationItemText { + get { + return ResourceManager.GetString("PullRequestsNavigationItemText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pulse. + /// + public static string PulseNavigationItemText { + get { + return ResourceManager.GetString("PulseNavigationItemText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This repository does not have a remote. Fill out the form to publish it to GitHub.. + /// + public static string RepoDoesNotHaveRemoteText { + get { + return ResourceManager.GetString("RepoDoesNotHaveRemoteText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Repository Name. + /// + public static string RepoNameText { + get { + return ResourceManager.GetString("RepoNameText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Repository created successfully.. + /// + public static string RepositoryPublishedMessage { + get { + return ResourceManager.GetString("RepositoryPublishedMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Resend. + /// + public static string resendCodeButtonContent { + get { + return ResourceManager.GetString("resendCodeButtonContent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Send the code to your registered SMS Device again. + /// + public static string resendCodeButtonToolTip { + get { + return ResourceManager.GetString("resendCodeButtonToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Retry. + /// + public static string Retry { + get { + return ResourceManager.GetString("Retry", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reviewers. + /// + public static string Reviewers { + get { + return ResourceManager.GetString("Reviewers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sign in.... + /// + public static string SignInCallToAction { + get { + return ResourceManager.GetString("SignInCallToAction", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sign in. + /// + public static string SignInLink { + get { + return ResourceManager.GetString("SignInLink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sign out. + /// + public static string SignOutLink { + get { + return ResourceManager.GetString("SignOutLink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sign up. + /// + public static string SignUpLink { + get { + return ResourceManager.GetString("SignUpLink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Switch to List View. + /// + public static string SwitchToListView { + get { + return ResourceManager.GetString("SwitchToListView", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Switch to Tree View. + /// + public static string SwitchToTreeView { + get { + return ResourceManager.GetString("SwitchToTreeView", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Welcome to GitHub for Visual Studio! Why not take a look at our [training](show-training) or [documentation](show-docs)? + /// + ///[Don't show this again](dont-show-again). + /// + public static string TeamExplorerWelcomeMessage { + get { + return ResourceManager.GetString("TeamExplorerWelcomeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Title (required). + /// + public static string TitleRequired { + get { + return ResourceManager.GetString("TitleRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Token. + /// + public static string TokenPrompt { + get { + return ResourceManager.GetString("TokenPrompt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Two-factor authentication. + /// + public static string twoFactorAuthText { + get { + return ResourceManager.GetString("twoFactorAuthText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Update comment. + /// + public static string UpdateComment { + get { + return ResourceManager.GetString("UpdateComment", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to updated {0}. + /// + public static string UpdatedFormat { + get { + return ResourceManager.GetString("UpdatedFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Username or email. + /// + public static string UserNameOrEmailPromptText { + get { + return ResourceManager.GetString("UserNameOrEmailPromptText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Verify. + /// + public static string verifyText { + get { + return ResourceManager.GetString("verifyText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to View Changes. + /// + public static string ViewChanges { + get { + return ResourceManager.GetString("ViewChanges", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to View Changes in Solution. + /// + public static string ViewChangesInSolution { + get { + return ResourceManager.GetString("ViewChangesInSolution", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to View File. + /// + public static string ViewFile { + get { + return ResourceManager.GetString("ViewFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wiki. + /// + public static string WikiNavigationItemText { + get { + return ResourceManager.GetString("WikiNavigationItemText", resourceCulture); + } + } + } +} diff --git a/src/GitHub.VisualStudio.UI/Resources.resx b/src/GitHub.VisualStudio.UI/Resources.resx new file mode 100644 index 0000000000..864d1f0dd1 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Resources.resx @@ -0,0 +1,449 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Invalid authentication code + + + Try entering the code again or clicking the resend button to get a new authentication code. + + + Authentication code sent! + + + If you do not receive the authentication code, contact support@github.com. + + + Browse + + + Could not connect to github.com + + + Could not connect to the server. + + + Create + + + Description (Optional) + + + Open in Browser + + + Cancel + + + Gist created + + + Failed to create gist + + + by + + + Privacy + + + Help us improve by sending anonymous usage data + + + Could not copy to the clipboard. Please try again. + + + Link copied to clipboard + + + Repository created successfully. + + + Private Gist + + + File Name + + + You are not signed in to {0}, so certain git operations may fail. [Sign in now]({1}) + + + Wiki + + + Pulse + + + Pull Requests + + + Path + + + Issues + + + Graphs + + + Publish to GitHub + + + Powerful collaboration, code review, and code management for open source and private projects. + + + Connect… + + + Clone + + + Verify + + + Two-factor authentication + + + Sign up + + + Sign out + + + Send the code to your registered SMS Device again + + + Resend + + + Repository Name + + + This repository does not have a remote. Fill out the form to publish it to GitHub. + + + Publish + + + or + + + Open the two-factor authentication app on your device to view your authentication code. + + + No repositories + + + Name + + + Private Repository + + + Sign in failed. + + + Local path + + + License + + + Learn more + + + Git ignore + + + Search repositories + + + Some or all repositories may not have loaded. Close the dialog and try again. + + + An error occurred while loading repositories + + + GitHub Enterprise server address + + + The host isn't available or is not a GitHub Enterprise server. Check the address and try again. + + + Username or email + + + Password + + + Check your username and password, then try again + + + Sign in + + + (forgot your password?) + + + Please check your internet connection and try again. + + + Don’t have GitHub Enterprise? + + + Don’t have an account? + + + Title (required) + + + Description + + + Publish this repository to GitHub and get powerful collaboration, code review, and code management for open source and private projects. + + + This repository is not on GitHub + + + No repository + + + We couldn't find a git repository here. Open a git project or click "File -> Add to Source Control" in a project to get started. + + + Create an account + + + Filter branches + + + Publish to GitHub + + + Get Started + + + Sign in + + + Sign in... + + + Local branch up to date + + + Changes ({0}) + + + View Changes + + + Compare File as Default Action + + + View File + + + Open File as Default Action + + + Switch to List View + + + Switch to Tree View + + + updated {0} + + + View Pull Request on GitHub + + + Welcome to GitHub for Visual Studio! Why not take a look at our [training](show-training) or [documentation](show-docs)? + +[Don't show this again](dont-show-again) + + + Updated + + + Show PR comments on editor margin + + + Experimental features + + + These features might change in a future version + + + View Changes in Solution + + + Open File in Solution + + + Token + + + Continue your review + + + Add your review + + + Reviewers + + + Add review comment + + + Add a single comment + + + Fork + + + Debugging + + + Enable Trace Logging + + + The GitHub extension is not available inside Blend + + + Show Fork button in Team Explorer + + + Update comment + + + Open + + + Are you sure you want to delete this comment? + + + Delete Comment + + + Retry + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio.UI/SharedDictionary.xaml b/src/GitHub.VisualStudio.UI/SharedDictionary.xaml new file mode 100644 index 0000000000..12da65ad03 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/SharedDictionary.xaml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio/Services/SharedResources.cs b/src/GitHub.VisualStudio.UI/SharedResources.cs similarity index 95% rename from src/GitHub.VisualStudio/Services/SharedResources.cs rename to src/GitHub.VisualStudio.UI/SharedResources.cs index 0c128a3435..b7dff21cb7 100644 --- a/src/GitHub.VisualStudio/Services/SharedResources.cs +++ b/src/GitHub.VisualStudio.UI/SharedResources.cs @@ -27,7 +27,6 @@ public static DrawingBrush GetDrawingForIcon(Octicon icon, Brush colorBrush, str Drawing = new GeometryDrawing() { Brush = colorBrush, - Pen = new Pen(colorBrush, 1.0).FreezeThis(), Geometry = OcticonPath.GetGeometryForIcon(icon).FreezeThis() } .FreezeThis(), diff --git a/src/GitHub.VisualStudio.UI/Styles/ActionLinkButton.xaml b/src/GitHub.VisualStudio.UI/Styles/ActionLinkButton.xaml new file mode 100644 index 0000000000..3d31e44cf7 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Styles/ActionLinkButton.xaml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio.UI/Styles/Buttons.xaml b/src/GitHub.VisualStudio.UI/Styles/Buttons.xaml new file mode 100644 index 0000000000..5dc263d17d --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Styles/Buttons.xaml @@ -0,0 +1,191 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio.UI/Styles/GitHubActionLink.xaml b/src/GitHub.VisualStudio.UI/Styles/GitHubActionLink.xaml new file mode 100644 index 0000000000..60218fc5ff --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Styles/GitHubActionLink.xaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio.UI/Styles/GitHubComboBox.xaml b/src/GitHub.VisualStudio.UI/Styles/GitHubComboBox.xaml new file mode 100644 index 0000000000..b3cbd32e7b --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Styles/GitHubComboBox.xaml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio.UI/Styles/GitHubTabControl.xaml b/src/GitHub.VisualStudio.UI/Styles/GitHubTabControl.xaml new file mode 100644 index 0000000000..5b9b6c0dc3 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Styles/GitHubTabControl.xaml @@ -0,0 +1,107 @@ + + + + + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio.UI/Styles/ItemsControls.xaml b/src/GitHub.VisualStudio.UI/Styles/ItemsControls.xaml new file mode 100644 index 0000000000..1433547dac --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Styles/ItemsControls.xaml @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio.UI/Styles/LinkDropDown.xaml b/src/GitHub.VisualStudio.UI/Styles/LinkDropDown.xaml new file mode 100644 index 0000000000..5a1eab240f --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Styles/LinkDropDown.xaml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio.UI/Styles/SectionControl.xaml b/src/GitHub.VisualStudio.UI/Styles/SectionControl.xaml new file mode 100644 index 0000000000..ff1dde984d --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Styles/SectionControl.xaml @@ -0,0 +1,67 @@ + + + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio.UI/Styles/TextBlocks.xaml b/src/GitHub.VisualStudio.UI/Styles/TextBlocks.xaml new file mode 100644 index 0000000000..58b18dedba --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Styles/TextBlocks.xaml @@ -0,0 +1,134 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio.UI/Styles/ThemeBlue.xaml b/src/GitHub.VisualStudio.UI/Styles/ThemeBlue.xaml new file mode 100644 index 0000000000..02e1b632f4 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Styles/ThemeBlue.xaml @@ -0,0 +1,69 @@ + + + + + + + #424242 + + + #FF0E70C0 + + + #FFCFD6E5 + + + #FF1B293E + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio.UI/Styles/ThemeDark.xaml b/src/GitHub.VisualStudio.UI/Styles/ThemeDark.xaml new file mode 100644 index 0000000000..b9994994e7 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Styles/ThemeDark.xaml @@ -0,0 +1,69 @@ + + + + + + + #424242 + + + #FF0097FB + + + #FF2D2D30 + + + #FFFFFFFF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio.UI/Styles/ThemeDesignTime.xaml b/src/GitHub.VisualStudio.UI/Styles/ThemeDesignTime.xaml new file mode 100644 index 0000000000..ca96be1e63 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Styles/ThemeDesignTime.xaml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio.UI/Styles/ThemeLight.xaml b/src/GitHub.VisualStudio.UI/Styles/ThemeLight.xaml new file mode 100644 index 0000000000..85fcdf79b5 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Styles/ThemeLight.xaml @@ -0,0 +1,69 @@ + + + + + + + #424242 + + + #FF0E70C0 + + + #FFEEEEF2 + + + #FF1B293E + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio.UI/Styles/VsBrush.xaml b/src/GitHub.VisualStudio.UI/Styles/VsBrush.xaml new file mode 100644 index 0000000000..1e965a6910 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Styles/VsBrush.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio.UI/Styles/VsBrushesBlue.xaml b/src/GitHub.VisualStudio.UI/Styles/VsBrushesBlue.xaml new file mode 100644 index 0000000000..ff1fc64ccd --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Styles/VsBrushesBlue.xaml @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GitHub.VisualStudio.UI/Styles/VsBrushesDark.xaml b/src/GitHub.VisualStudio.UI/Styles/VsBrushesDark.xaml new file mode 100644 index 0000000000..b069dd81af --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Styles/VsBrushesDark.xaml @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GitHub.VisualStudio.UI/Styles/VsBrushesLight.xaml b/src/GitHub.VisualStudio.UI/Styles/VsBrushesLight.xaml new file mode 100644 index 0000000000..23baf3a4c3 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Styles/VsBrushesLight.xaml @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GitHub.VisualStudio.UI/Styles/VsColorsBlue.xaml b/src/GitHub.VisualStudio.UI/Styles/VsColorsBlue.xaml new file mode 100644 index 0000000000..e5f28cb840 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Styles/VsColorsBlue.xaml @@ -0,0 +1,509 @@ + + #FFE5C365 + #FFC0A776 + #FFFFF0D0 + #FFFFECB5 + #FFDEE1E7 + #FFB4B4B4 + #FF99B4D1 + #FFABABAB + #FFE8E8EC + #FF293955 + #FF293955 + #FF465A7D + #FF293955 + #FF293955 + #FF9BA7B7 + #FFFFFFFF + #FFFFFFFF + #FF000000 + #FFFFFFFF + #FF8591A2 + #FFC8D5E8 + #FF1B293E + #FF000000 + #FFF0F0F0 + #FFFFFFFF + #FFA0A0A0 + #FF000000 + #FF000000 + #FFF0F2F9 + #FFD3DCEF + #FFCCCC66 + #FFFFFFCC + #FF000000 + #FFD2D2D2 + #FF808080 + #FF000000 + #FFFFFFFF + #FF00008B + #FF000000 + #FF000000 + #FF000000 + #FFFFFFFF + #FFF7F0F0 + #FFEDDADC + #FFFFFFFF + #FF0054E3 + #FFDDD6EF + #FF266035 + #FFFFFFFF + #FF716F64 + #FFF3F7F0 + #FFE6F0DB + #FF808080 + #FF716F64 + #FFB0764F + #FF716F64 + #FF808080 + #FF716F64 + #FFD8D8D8 + #FF808080 + #FF716F64 + #FFD6ECEF + #FFFF0000 + #FFF8F4E9 + #FFF0E9D2 + #FFFCFCFC + #FFBDC5D8 + #FFD5DCE8 + #FFBDC5D8 + #FFA4ADBA + #FF1B293E + #FFFCFCFC + #FFE5C365 + #FFFCFCFC + #FFFCFCFC + #FFFCFCFC + #FFFCFCFC + #FFE5C365 + #FF000000 + #FFEFEFEF + #FFEFEFEF + #FF9BA7B7 + #FFE5C365 + #FF000000 + #FF60728C + #FFBCC7D8 + #FFCFD6E5 + #FFCFD6E5 + #FFCFD6E5 + #FFFDF4BF + #FFFFF29D + #FFFFFCF4 + #FFE5C365 + #FFEAF0FF + #FFEAF0FF + #FF9BA7B7 + #FFF2F4FE + #FF000000 + #FFBEC3CB + #FF000000 + #FFFFF29D + #FFFFF29D + #FFFFF29D + #FFE5C365 + #FFFDF4BF + #FFFDF4BF + #FFFDF4BF + #FFFDF4BF + #FFDCE0EC + #FF1B293E + #FFFFF29D + #FFFFF29D + #FFFFF29D + #FFFDF4BF + #FFFDF4BF + #FFFDF4BF + #FFFDF4BF + #FF1B293E + #FFFDF4BF + #FFE5C365 + #FFD6DBE9 + #FF1B293E + #FF000000 + #FF808080 + #FF000000 + #FFDCE0EC + #FF8591A2 + #FFD6DBE9 + #FFD6DBE9 + #FFD6DBE9 + #FFD6DBE9 + #FFD6DBE9 + #FFD6DBE9 + #FFA0A0A0 + #FFFFFAC8 + #FF3C7FB1 + #FF0066CC + #FF3399FF + #FF3399FF + #FF8591A2 + #00CCCEDB + #FFFFFFFF + #FF8591A2 + #FFBCC7D8 + #FF000000 + #FFA8B3C2 + #FF000000 + #FFF0F0F0 + #FFA4ADBA + #FFBCC7D8 + #FF808080 + #FFA4ADBA + #FF808080 + #FFFFFFFF + #FF716F64 + #FFDEE1E7 + #FF808080 + #FFFFFFFF + #FFF2F4F8 + #FF000000 + #FF4A6184 + #FF4A6184 + #FFBCC7D8 + #FFFFFFFF + #FF000000 + #FFFFFFFF + #FF636871 + #FFF5F8FB + #FFDEE2E9 + #FF8A919C + #FF445879 + #FFFDE8A7 + #FFF7C570 + #FF445879 + #FFFCFCFC + #FFBDC5D8 + #FFD5DCE8 + #FFBDC5D8 + #FFA4ADBA + #FF1B293E + #FFFCFCFC + #FFE5C365 + #FFFCFCFC + #FFFCFCFC + #FFFCFCFC + #FFFCFCFC + #FFE5C365 + #FF000000 + #FFEFEFEF + #FFEFEFEF + #FF9BA7B7 + #72000000 + #FFC0A776 + #FFDEE1E7 + #FF0066CC + #FF000000 + #FF293955 + #FF293955 + #FF293955 + #FF35496A + #FF35496A + #FF293955 + #FF35496A + #FFFFFF00 + #FFFF9200 + #FFF0F0EE + #FFA9A9A9 + #FF364E6F + #FF364E6F + #FFFFF29D + #FF364E6F + #FF364E6F + #FF293955 + #FF293955 + #FF5B7199 + #FFCED4DD + #FF5B7199 + #FF5B7199 + #FFFFFFFF + #FF4D6082 + #FF4D6082 + #FF4D6082 + #FF4D6082 + #FFFFFFFF + #FFCED4DF + #FFC0C9D9 + #FF5F6673 + #FFD5DAE3 + #FFD5DAE3 + #FFD5DAE3 + #FFD5DAE3 + #FF000000 + #FF293955 + #FFFFF29D + #FFFFF29D + #FFFFF29D + #FFFFF29D + #FFFFF29D + #FF000000 + #FFFFFFFF + #FF000000 + #FFFFFFFF + #FF000000 + #FFFFFFFF + #FF6D6D6D + #FFF0F0F0 + #FF000000 + #FFF0F0F0 + #FFDEE1E7 + #FF0066CC + #FF000000 + #FFFFFFFF + #FF0066CC + #FF000000 + #FFFFFFFF + #FFA8B3C2 + #FFFFFFFF + #FFA8B3C2 + #FF000000 + #FFDEE1E7 + #FF000000 + #FFA8B3C2 + #FF000000 + #FFFFFFFF + #FF000000 + #FFDEE1E7 + #FF1B293E + #FF0066CC + #FF0066CC + #FFF0F0F0 + #FF000000 + #FF000000 + #FF0078D7 + #FFFFFFFF + #FFF4F7FC + #FFBFCDDB + #FF000000 + #FFFFFFE1 + #FF000000 + #00F6F6F6 + #001E1E1E + #FFA8B3C2 + #00EFEFF2 + #FFF0F0F0 + #FF000000 + #FFBCC7D8 + #FFEEEDED + #FFCFCFCF + #FFDDDDDD + #FFFFE8A6 + #FFE5C365 + #FFFFFCF4 + #FFFFECB5 + #FF000000 + #FFFFF3CD + #FFFFECB5 + #FF4D6082 + #FF3D5277 + #FFFFFFFF + #FF4A6184 + #FFA8B3C2 + #FFF2F4F8 + #FF4A6184 + #FFDEE1E7 + #FFFFF3CD + #FFFFECB5 + #FFDEE1E7 + #FF000000 + #FF8591A2 + #FFDEE1E7 + #FFFFFFFF + #FFE5C365 + #FFFFFCF4 + #FF0066CC + #FF3399FF + #FF3399FF + #FFA8B3C2 + #FFA8B3C2 + #FF1B293E + #FFCDD4DF + #FF1B293E + #FFBCC7D8 + #FFBCC7D8 + #FFBCC7D8 + #FFA8B3C2 + #FFA8B3C2 + #FFF0F0F0 + #FFF0F0F0 + #FFDEE1E7 + #FFFFFFFF + #FFA8B3C2 + #FFC0A776 + #FFFFE8A6 + #FFF0F0F0 + #FFDEE1E7 + #FFDEE1E7 + #FFF0F0F0 + #FFDEE1E7 + #FFFFFFE1 + #FF000000 + #FF000000 + #FFE8E8EC + #FFE8E8EC + #FFE8E8EC + #FFE8E8EC + #FFE8E8EC + #FFE8E8EC + #FFE8E8EC + #FFC2C3C9 + #FFC2C3C9 + #FFC2C3C9 + #FF686868 + #FF5B5B5B + #FFFFFFFF + #FF8591A2 + #FFFDF4BF + #FFFDF4BF + #FFFDF4BF + #FFFDF4BF + #FFE5C365 + #FFFFF29D + #FFE5C365 + #FFDEE1E7 + #FFDEE1E7 + #FFDEE1E7 + #FF1B293E + #FFE5C365 + #FFFFF0D0 + #FFE5C365 + #FFFFECB5 + #FF000000 + #FF000000 + #FF4169E1 + #FF96A9DD + #FFE122DF + #FFF0F0F0 + #FF000000 + #FF8591A2 + #FF162030 + #FF162030 + #FF999999 + #FFF1F1F1 + #FFF1F1F1 + #FFF1F1F1 + #FFF1F1F1 + #FF464646 + #FF464646 + #FFF30506 + #FF0097FB + #FF55AAFF + #FFF30506 + #FF007ACC + #FFFFFFFF + #FF363639 + #FFFFFFFF + #FFFFFFFF + #FFFFFFFF + #FFFFFFFF + #FFF1F1F1 + #FFF30506 + #FF555555 + #FF007ACC + #FF77AAFF + #FF1E1E1E + #FF999999 + #FF007ACC + #FF555555 + #FF999999 + #FF007ACC + #FF000000 + #FF3F3F3F + #FF464646 + #FF999999 + #FFFFFFFF + #FFF0F0F0 + #FF696969 + #FFF0F0F0 + #FFFFFFFF + #FFE3E3E3 + #FFA0A0A0 + #FFFFF29D + #FFFFF29D + #FFFFF29D + #FFFFF29D + #FFFFF29D + #FF000000 + #FF4D6082 + #FF4D6082 + #FF4D6082 + #FFFFFFFF + #FFFFFFFF + #FF8591A2 + #FFFFFFFF + #FFFFFFFF + #FFCED4DF + #FFF0F0F0 + #FFF0F0F0 + #FFF7F7FF + #FFA0A0A0 + #FFFFFBF0 + #FFFFF2CB + #FFFFF7DA + #FFFFF2CB + #FFFFFFFF + #FF8E9BBC + #FF75633D + #FFFFE8A6 + #FF000000 + #FFE5C365 + #FF000000 + #FFFFFCF4 + #FFE5C365 + #FF000000 + #FFFFFCF4 + #FFE5C365 + #FF000000 + #FF2F405E + #FF707E96 + #FFCED4DD + #FFFBFCFD + #FFFBFCFD + #FF293955 + #FF4B5C74 + #FF4D6082 + #FF4D6082 + #FF5B7199 + #FF5B7199 + #FF5B7199 + #FFFFFFFF + #FFFFFFFF + #FF000000 + #FFFFFFFF + #FF000000 + #FF705829 + #FFB0A781 + #FFA19667 + #FFA79432 + #FFD0D4B7 + #FFBFC749 + #FFCAB22D + #FFFBF7C8 + #FFE2E442 + #FF5D8039 + #FFB1C97B + #FF9FB861 + #FF8E5478 + #FFE2B1CD + #FFCB98B6 + #FFAD1C2B + #FFFF9F99 + #FFFF7971 + #FF779AB6 + #FFC6D4DF + #FFB8CCD7 + #FF427094 + #FFA0B7C9 + #FF89ABBD + #FF5386BF + #FFB9D4EE + #FFA1C7E7 + #FFFFFFFF + #FF646464 + #FF000000 + #FFFFFFFF + #FF000000 + diff --git a/src/GitHub.VisualStudio.UI/Styles/VsColorsDark.xaml b/src/GitHub.VisualStudio.UI/Styles/VsColorsDark.xaml new file mode 100644 index 0000000000..6f9bb15af6 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Styles/VsColorsDark.xaml @@ -0,0 +1,509 @@ + + #FF3F3F46 + #FF3F3F46 + #FF252526 + #FF2D2D30 + #FF2D2D30 + #FF3F3F46 + #FF252526 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF3F3F46 + #FF2D2D30 + #FF2D2D30 + #FF007ACC + #FF0097FB + #FFD0D0D0 + #FF000000 + #FF2D2D30 + #FF3F3F46 + #FF252526 + #FFF1F1F1 + #FFF1F1F1 + #FF3F3F46 + #FF464646 + #FF3F3F46 + #FFF1F1F1 + #FFF1F1F1 + #FFF0F2F9 + #FFD3DCEF + #FFCCCC66 + #FFFFFFCC + #FF000000 + #FFD2D2D2 + #FF808080 + #FF000000 + #FFFFFFFF + #FF00008B + #FF000000 + #FF000000 + #FF000000 + #FFFFFFFF + #FFF7F0F0 + #FFEDDADC + #FFFFFFFF + #FF0054E3 + #FFDDD6EF + #FF266035 + #FFFFFFFF + #FF716F64 + #FFF3F7F0 + #FFE6F0DB + #FF808080 + #FF716F64 + #FFB0764F + #FF716F64 + #FF808080 + #FF716F64 + #FFD8D8D8 + #FF808080 + #FF716F64 + #FFD6ECEF + #FFFF0000 + #FFF8F4E9 + #FFF0E9D2 + #FF333337 + #FF434346 + #FF2D2D30 + #FF434346 + #FF434346 + #FFF1F1F1 + #FF3F3F46 + #FF007ACC + #FF3F3F46 + #FF3F3F46 + #FF3F3F46 + #FF3F3F46 + #FF007ACC + #FF007ACC + #FF1B1B1C + #FF1B1B1C + #FF333337 + #FF2D2D30 + #FF999999 + #FF46464A + #FF46464A + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF3E3E40 + #FF3E3E40 + #FF3E3E40 + #FF3399FF + #FF1B1B1C + #FF1B1B1C + #FF333337 + #FF1B1B1C + #FF007ACC + #FF333337 + #FF999999 + #FF007ACC + #FF007ACC + #FF007ACC + #FF007ACC + #FF3E3E40 + #FF3E3E40 + #FF3E3E40 + #FF3E3E40 + #FF2D2D30 + #FF999999 + #FF007ACC + #FF007ACC + #FF007ACC + #72555555 + #72555555 + #72555555 + #72555555 + #FF007ACC + #FF2D2D30 + #FF3399FF + #FF2D2D30 + #FFF1F1F1 + #FFF1F1F1 + #FF656565 + #FFF1F1F1 + #FF2D2D30 + #FF222222 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF999999 + #FFFEFCC8 + #FF555555 + #FF0097FB + #FF0097FB + #FF0097FB + #FF333337 + #003F3F46 + #FF424245 + #FF4D4D50 + #FF505051 + #FFF1F1F1 + #FF333337 + #FFF1F1F1 + #FF2C2C2F + #FF37373A + #FF3D3D3F + #FF656565 + #FF333337 + #FF656565 + #FF252526 + #FF46464A + #FF3F3F46 + #FF656565 + #FFFFFFFF + #FFF2F4F8 + #FF000000 + #FF4A6184 + #FF4A6184 + #FFBCC7D8 + #FFFFFFFF + #FF000000 + #FF1B1B1C + #FF333337 + #FF252526 + #FF252526 + #FF252526 + #FFF1F1F1 + #FF252526 + #FF252526 + #FF007ACC + #FF333337 + #FF434346 + #FF2D2D30 + #FF434346 + #FF434346 + #FF999999 + #FF3F3F46 + #FF434346 + #FF3F3F46 + #FF3F3F46 + #FF3F3F46 + #FF3F3F46 + #FF434346 + #FF007ACC + #FF1B1B1C + #FF1B1B1C + #FF333337 + #72000000 + #FF333337 + #FF252526 + #FF0097FB + #FFF1F1F1 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FFFF8C00 + #FFFF8C00 + #FF656565 + #FF656565 + #FF2D2D30 + #FF2D2D30 + #FF007ACC + #FF007ACC + #FF007ACC + #FF2D2D30 + #FF2D2D30 + #FF1C97EA + #FFD0E6F5 + #FF1C97EA + #FF1C97EA + #FFFFFFFF + #FF3F3F46 + #FF3F3F46 + #FF3F3F46 + #FF3F3F46 + #FFF1F1F1 + #FF007ACC + #FF007ACC + #FFD0E6F5 + #FF0E639C + #FF0E639C + #FF0E639C + #FF0E639C + #FFFFFFFF + #FF007ACC + #FF007ACC + #FF007ACC + #FF007ACC + #FF007ACC + #FF007ACC + #FFFFFFFF + #FFF1F1F1 + #FF000000 + #FFFFFFFF + #FF000000 + #FFFFFFFF + #FF999999 + #FF2D2D30 + #FFF1F1F1 + #FF000000 + #FFDEE1E7 + #FF0066CC + #FF000000 + #FFFFFFFF + #FF0066CC + #FF000000 + #FFFFFFFF + #FFA8B3C2 + #FFFFFFFF + #FFA8B3C2 + #FF000000 + #FFDEE1E7 + #FF000000 + #FFA8B3C2 + #FF000000 + #FFFFFFFF + #FF000000 + #FFDEE1E7 + #FF1B293E + #FF0066CC + #FF0066CC + #FFF0F0F0 + #FF000000 + #FF000000 + #FF3399FF + #FFFFFFFF + #FF3F3F46 + #FF2D2D30 + #FF656565 + #FFFEFCC8 + #FF1E1E1E + #001E1E1E + #00F1F1F1 + #FF333337 + #002D2D30 + #FF1B1B1C + #FFF1F1F1 + #FF252526 + #FF3F3F46 + #FF3F3F46 + #FF3F3F46 + #FF3399FF + #FF3399FF + #FF3E3E40 + #FF3E3E40 + #FFF1F1F1 + #FF3E3E40 + #FF3E3E40 + #FF3F3F46 + #FF3F3F46 + #FFF1F1F1 + #FFF1F1F1 + #FF3F3F46 + #FF252526 + #FFF1F1F1 + #FF3E3E40 + #FF3F3F46 + #FF3F3F46 + #FF3F3F46 + #FFF1F1F1 + #FF333337 + #FF252526 + #FF252526 + #FF3E3E40 + #FF3E3E40 + #FF0097FB + #FF55AAFF + #FF0097FB + #FF2D2D30 + #FF2D2D30 + #FFF1F1F1 + #FF252526 + #FFF1F1F1 + #FF252526 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF3399FF + #FF3399FF + #FF3399FF + #FF3399FF + #FF3399FF + #FF2D2D30 + #FF252526 + #FF252526 + #FF2D2D30 + #FFFEFCC8 + #FFFEFCC8 + #FF252526 + #FF3E3E42 + #FF3E3E42 + #FF3E3E42 + #FF3E3E42 + #FF3E3E42 + #FF3E3E42 + #FF3E3E42 + #FF686868 + #FF686868 + #FF686868 + #FF9E9E9E + #FFEFEBEF + #FF333337 + #FF333337 + #FF3F3F46 + #FF3F3F46 + #FF3F3F46 + #FF3F3F46 + #FF3F3F46 + #FF3F3F46 + #FF007ACC + #FF252526 + #FF252526 + #FF252526 + #FFF1F1F1 + #FFE5C365 + #FFFFEFBB + #FFE5C365 + #FFFEFCC8 + #FF000000 + #FF000000 + #FF4169E1 + #FF96A9DD + #FFE122DF + #FF252526 + #FFF1F1F1 + #FF434346 + #FF1F1F22 + #FF1F1F22 + #FF999999 + #FF464646 + #FF464646 + #FF464646 + #FF464646 + #FF464646 + #FF464646 + #FFF30506 + #FF0097FB + #FF55AAFF + #FFF30506 + #FF007ACC + #FFFFFFFF + #FF363639 + #FF252526 + #FF252526 + #FF28282B + #FF28282B + #FFF1F1F1 + #FFF30506 + #FFF1F1F1 + #FF0097FB + #FF88CCFE + #FFF1F1F1 + #FF999999 + #FF55AAFF + #FFF1F1F1 + #FF999999 + #FF55AAFF + #FFF1F1F1 + #FF3F3F3F + #FF464646 + #FF999999 + #FFFFFFFF + #FF000000 + #FF2D2D30 + #FF3F3F46 + #FF464646 + #FF2D2D30 + #FF3F3F46 + #FF2D2D30 + #FF007ACC + #FF007ACC + #FF007ACC + #FF007ACC + #FFFFFFFF + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FFD0D0D0 + #FF252526 + #FF333337 + #FF252526 + #FF252526 + #FF252526 + #FF252526 + #FF252526 + #FF252526 + #FF252526 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF252526 + #FF3F3F46 + #FFFFFFFF + #FF0E6198 + #FFFFFFFF + #FF0E6198 + #FFFFFFFF + #FF52B0EF + #FF52B0EF + #FFFFFFFF + #FF393939 + #FF393939 + #FFF1F1F1 + #FF2D2D30 + #FF2D2D30 + #FFF1F1F1 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF2D2D30 + #FF3E3E40 + #FF3E3E40 + #FF3E3E40 + #FF55AAFF + #FF252526 + #FF0097FB + #FFD0D0D0 + #FFF1F1F1 + #FF705829 + #FFB0A781 + #FFA19667 + #FFA79432 + #FFD0D4B7 + #FFBFC749 + #FFCAB22D + #FFFBF7C8 + #FFE2E442 + #FF5D8039 + #FFB1C97B + #FF9FB861 + #FF8E5478 + #FFE2B1CD + #FFCB98B6 + #FFAD1C2B + #FFFF9F99 + #FFFF7971 + #FF779AB6 + #FFC6D4DF + #FFB8CCD7 + #FF427094 + #FFA0B7C9 + #FF89ABBD + #FF5386BF + #FFB9D4EE + #FFA1C7E7 + #FF252526 + #FF2D2D30 + #FFF1F1F1 + #FFFFFFFF + #FF000000 + diff --git a/src/GitHub.VisualStudio.UI/Styles/VsColorsLight.xaml b/src/GitHub.VisualStudio.UI/Styles/VsColorsLight.xaml new file mode 100644 index 0000000000..cd4f4de68d --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Styles/VsColorsLight.xaml @@ -0,0 +1,509 @@ + + #FFCCCEDB + #FFCCCEDB + #FFF5F5F5 + #FFEEEEF2 + #FFEEEEF2 + #FFCCCEDB + #FFF5F5F5 + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFCCCEDB + #FFEEEEF2 + #FFEEEEF2 + #FF007ACC + #FF0E70C0 + #FF444444 + #FFFFFFFF + #FFEEEEF2 + #FFCCCEBD + #FFF5F5F5 + #FF1E1E1E + #FF1E1E1E + #FFCCCEDB + #FFD8D8E0 + #FFCCCEBD + #FF1E1E1E + #FF1E1E1E + #FFF0F2F9 + #FFD3DCEF + #FFCCCC66 + #FFFFFFCC + #FF000000 + #FFD2D2D2 + #FF808080 + #FF000000 + #FFFFFFFF + #FF00008B + #FF000000 + #FF000000 + #FF000000 + #FFFFFFFF + #FFF7F0F0 + #FFEDDADC + #FFFFFFFF + #FF0054E3 + #FFDDD6EF + #FF266035 + #FFFFFFFF + #FF716F64 + #FFF3F7F0 + #FFE6F0DB + #FF808080 + #FF716F64 + #FFB0764F + #FF716F64 + #FF808080 + #FF716F64 + #FFD8D8D8 + #FF808080 + #FF716F64 + #FFD6ECEF + #FFFF0000 + #FFF8F4E9 + #FFF0E9D2 + #FFFFFFFF + #FFCCCEDB + #FFEEEEF2 + #FFCCCEDB + #FFCCCEDB + #FF717171 + #FFFFFFFF + #FF007ACC + #FFFFFFFF + #FFFFFFFF + #FFFFFFFF + #FFFFFFFF + #FF007ACC + #FF1E1E1E + #FFF6F6F6 + #FFF6F6F6 + #FFCCCEDB + #FFEEEEF2 + #FF717171 + #FF999999 + #FF999999 + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFC9DEF5 + #FFC9DEF5 + #FFC9DEF5 + #FF3399FF + #FFF6F6F6 + #FFF6F6F6 + #FFCCCEDB + #FFF6F6F6 + #FF007ACC + #FFE0E3E6 + #FF717171 + #FF007ACC + #FF007ACC + #FF007ACC + #FF007ACC + #FFC9DEF5 + #FFC9DEF5 + #FFC9DEF5 + #FFC9DEF5 + #FFEEEEF2 + #FF717171 + #FF007ACC + #FF007ACC + #FF007ACC + #FFC9DEF5 + #FFC9DEF5 + #FFC9DEF5 + #FFC9DEF5 + #FF007ACC + #FFEEEEF2 + #FF3399FF + #FFEEEEF2 + #FF1E1E1E + #FF1E1E1E + #FFA2A4A5 + #FF1E1E1E + #FFEEEEF2 + #FFCCCEDB + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FF717171 + #FFFDFBAC + #FF717171 + #FF0E70C0 + #FF0E70C0 + #FF0E70C0 + #FFCCCEDB + #00CCCEDB + #FFE7E8EC + #FFCCCEDB + #FFEDEEF0 + #FF1E1E1E + #FFCCCEDB + #FF1E1E1E + #FFD6D8DC + #FFCCCEDB + #FFEDEEF0 + #FFA2A4A5 + #FFCCCEDB + #FFA2A4A5 + #FFF5F5F5 + #FF999999 + #FFCCCEDB + #FFA2A4A5 + #FFFFFFFF + #FFF2F4F8 + #FF000000 + #FF4A6184 + #FF4A6184 + #FFBCC7D8 + #FFFFFFFF + #FF000000 + #FFE7E8EC + #FFCCCEDB + #FFF5F5F5 + #FFF5F5F5 + #FFF5F5F5 + #FF1E1E1E + #FFF5F5F5 + #FFF5F5F5 + #FF007ACC + #FFFFFFFF + #FFCCCEDB + #FFEEEEF2 + #FFCCCEDB + #FFCCCEDB + #FF717171 + #FFFFFFFF + #FF007ACC + #FFFFFFFF + #FFFFFFFF + #FFFFFFFF + #FFFFFFFF + #FF007ACC + #FF1E1E1E + #FFF6F6F6 + #FFF6F6F6 + #FFCCCEDB + #72000000 + #FFCCCEDB + #FFF5F5F5 + #FF0E70C0 + #FF1E1E1E + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFFFA300 + #FFFFA300 + #FFA2A4A5 + #FFA2A4A5 + #FFEEEEF2 + #FFEEEEF2 + #FF007ACC + #FF007ACC + #FF007ACC + #FFEEEEF2 + #FFEEEEF2 + #FF1C97EA + #FFD0E6F5 + #FF1C97EA + #FF1C97EA + #FFFFFFFF + #FFCCCEDB + #FFCCCEDB + #FFCCCEDB + #FFCCCEDB + #FF717171 + #FF007ACC + #FF007ACC + #FFD0E6F5 + #FF0E639C + #FF0E639C + #FF0E639C + #FF0E639C + #FFFFFFFF + #FF007ACC + #FF007ACC + #FF007ACC + #FF007ACC + #FF007ACC + #FF007ACC + #FFFFFFFF + #FF1E1E1E + #FF000000 + #FFFFFFFF + #FF000000 + #FFFFFFFF + #FF717171 + #FFEFEFE2 + #FF1E1E1E + #FFF0F0F0 + #FFDEE1E7 + #FF0066CC + #FF000000 + #FFFFFFFF + #FF0066CC + #FF000000 + #FFFFFFFF + #FFA8B3C2 + #FFFFFFFF + #FFA8B3C2 + #FF000000 + #FFDEE1E7 + #FF000000 + #FFA8B3C2 + #FF000000 + #FFFFFFFF + #FF000000 + #FFDEE1E7 + #FF1B293E + #FF0066CC + #FF0066CC + #FFF0F0F0 + #FF000000 + #FF000000 + #FF3399FF + #FFFFFFFF + #FFCCCEDB + #FFEEEEF2 + #FFA2A4A5 + #FFFDFBAC + #FF1E1E1E + #00F5F5F5 + #001E1E1E + #FFCCCEDB + #00EEEEF2 + #FFF6F6F6 + #FF1E1E1E + #FFF5F5F5 + #FFCCCEDB + #FFCCCEDB + #FFCCCEDB + #FF3399FF + #FF3399FF + #FFFEFEFE + #FFFEFEFE + #FF1E1E1E + #FFFEFEFE + #FFFEFEFE + #FFCCCEDB + #FFCCCEDB + #FF1E1E1E + #FF1E1E1E + #FFCCCEDB + #FFF5F5F5 + #FF1E1E1E + #FFFEFEFE + #FFCCCEDB + #FFCCCEDB + #FFCCCEDB + #FF1E1E1E + #FFCCCEDB + #FFF5F5F5 + #FFF5F5F5 + #FFFEFEFE + #FFFEFEFE + #FF0E70C0 + #FF007ACC + #FF0E70C0 + #FFEEEEF2 + #FFEEEEF2 + #FF1E1E1E + #FFF5F5F5 + #FF1E1E1E + #FFF5F5F5 + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FF3399FF + #FF3399FF + #FF3399FF + #FF3399FF + #FF3399FF + #FFEEEEF2 + #FFF5F5F5 + #FFF5F5F5 + #FFEEEEF2 + #FFFDFBAC + #FFFDFBAC + #FF1E1E1E + #FFF5F5F5 + #FFF5F5F5 + #FFF5F5F5 + #FFF5F5F5 + #FFF5F5F5 + #FFF5F5F5 + #FFF5F5F5 + #FFC2C3C9 + #FFC2C3C9 + #FFC2C3C9 + #FF686868 + #FF5B5B5B + #FFFCFCFC + #FFFCFCFC + #FFFFFFFF + #FFFFFFFF + #FFFFFFFF + #FFFFFFFF + #FFFFFFFF + #FFFFFFFF + #FF007ACC + #FFF5F5F5 + #FFF5F5F5 + #FFF5F5F5 + #FF1E1E1E + #FFE5C365 + #FFFFEFBB + #FFE5C365 + #FFFDFBAC + #FF000000 + #FF000000 + #FF4169E1 + #FF96A9DD + #FFE122DF + #FFF5F5F5 + #FF1E1E1E + #FF999999 + #FF2D2D30 + #FF2D2D30 + #FF999999 + #FFF1F1F1 + #FFF1F1F1 + #FFF1F1F1 + #FFF1F1F1 + #FF464646 + #FF464646 + #FFF30506 + #FF0097FB + #FF55AAFF + #FFF30506 + #FF007ACC + #FFFFFFFF + #FF363639 + #FFFFFFFF + #FFFFFFFF + #FFFFFFFF + #FFFFFFFF + #FFF1F1F1 + #FFF30506 + #FF555555 + #FF007ACC + #FF77AAFF + #FF1E1E1E + #FF999999 + #FF007ACC + #FF555555 + #FF999999 + #FF007ACC + #FF000000 + #FF3F3F3F + #FF464646 + #FF999999 + #FFFFFFFF + #FFF0F0F0 + #FFEEEEF2 + #FFCCCEDB + #FFD8D8E0 + #FFEEEEF2 + #FFCCCEBD + #FFEEEEF2 + #FF007ACC + #FF007ACC + #FF007ACC + #FF007ACC + #FFFFFFFF + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FF444444 + #FFF5F5F5 + #FFCCCEDB + #FFF5F5F5 + #FFF5F5F5 + #FFF5F5F5 + #FFF5F5F5 + #FFF5F5F5 + #FFF5F5F5 + #FFF5F5F5 + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFF5F5F5 + #FFCCCEDB + #FFFFFFFF + #FF0E6198 + #FFFFFFFF + #FF0E6198 + #FFFFFFFF + #FF52B0EF + #FF52B0EF + #FFFFFFFF + #FFF7F7F9 + #FFF7F7F9 + #FF717171 + #FFF5F5F5 + #FFF5F5F5 + #FF1E1E1E + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFEEEEF2 + #FFC9DEF5 + #FFC9DEF5 + #FFC9DEF5 + #FF1E1E1E + #FFF5F5F5 + #FF0E70C0 + #FF444444 + #FF1E1E1E + #FF705829 + #FFB0A781 + #FFA19667 + #FFA79432 + #FFD0D4B7 + #FFBFC749 + #FFCAB22D + #FFFBF7C8 + #FFE2E442 + #FF5D8039 + #FFB1C97B + #FF9FB861 + #FF8E5478 + #FFE2B1CD + #FFCB98B6 + #FFAD1C2B + #FFFF9F99 + #FFFF7971 + #FF779AB6 + #FFC6D4DF + #FFB8CCD7 + #FF427094 + #FFA0B7C9 + #FF89ABBD + #FF5386BF + #FFB9D4EE + #FFA1C7E7 + #FFF5F5F5 + #FFEEEEF2 + #FF1E1E1E + #FFFFFFFF + #FF000000 + diff --git a/src/GitHub.VisualStudio.UI/TeamFoundationColors.cs b/src/GitHub.VisualStudio.UI/TeamFoundationColors.cs new file mode 100644 index 0000000000..908896157d --- /dev/null +++ b/src/GitHub.VisualStudio.UI/TeamFoundationColors.cs @@ -0,0 +1,61 @@ +using System; +using System.ComponentModel; +using System.Windows.Media; +using Microsoft.VisualStudio.PlatformUI; +using Microsoft.VisualStudio.Shell; + +namespace GitHub.VisualStudio.UI +{ + /// + /// Retrieves themed Team Foundation colors. + /// + public class TeamFoundationColors : INotifyPropertyChanged + { + static readonly Guid VsTeamFoundationColorsCategory = new Guid("4aff231b-f28a-44f0-a66b-1beeb17cb920"); + static Lazy instance = new Lazy(() => new TeamFoundationColors()); + + /// + /// Initializes a new instance of the class. + /// + private TeamFoundationColors() + { + VSColorTheme.ThemeChanged += _ => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null)); + } + + /// + /// Gets a singleton instance of the class. + /// + public static TeamFoundationColors Instance => instance.Value; + + /// + /// Gets the Team Foundation "RequiredTextBoxBorder" color. + /// + public Color RequiredTextBoxBorderColor => GetColor("RequiredTextBoxBorder"); + + /// + /// Gets the Team Foundation "TextBoxBorder" color. + /// + public Color TextBoxBorderColor => GetColor("TextBoxBorder"); + + /// + /// Gets the Team Foundation "TextBox" color. + /// + public Color TextBoxColor => GetColor("TextBox"); + + /// + /// Gets the Team Foundation "TextBoxHintText" color. + /// + public Color TextBoxHintTextColor => GetColor("TextBoxHintText"); + + /// + /// Occurs when a property on the object changes. + /// + public event PropertyChangedEventHandler PropertyChanged; + + private static Color GetColor(string name) + { + var c = VSColorTheme.GetThemedColor(new ThemeResourceKey(VsTeamFoundationColorsCategory, name, ThemeResourceKeyType.BackgroundColor)); + return Color.FromArgb(c.A, c.R, c.G, c.B); + } + } +} diff --git a/src/GitHub.VisualStudio.UI/UI/Controls/AccountAvatar.xaml b/src/GitHub.VisualStudio.UI/UI/Controls/AccountAvatar.xaml new file mode 100644 index 0000000000..ef3defbc5d --- /dev/null +++ b/src/GitHub.VisualStudio.UI/UI/Controls/AccountAvatar.xaml @@ -0,0 +1,30 @@ + + + diff --git a/src/GitHub.VisualStudio.UI/UI/Controls/AccountAvatar.xaml.cs b/src/GitHub.VisualStudio.UI/UI/Controls/AccountAvatar.xaml.cs new file mode 100644 index 0000000000..7570da7e40 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/UI/Controls/AccountAvatar.xaml.cs @@ -0,0 +1,52 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using GitHub.Models; + +namespace GitHub.VisualStudio.UI.Controls +{ + public partial class AccountAvatar : UserControl, ICommandSource + { + public static readonly DependencyProperty AccountProperty = + DependencyProperty.Register( + nameof(Account), + typeof(object), + typeof(AccountAvatar)); + public static readonly DependencyProperty CommandProperty = + ButtonBase.CommandProperty.AddOwner(typeof(AccountAvatar)); + public static readonly DependencyProperty CommandParameterProperty = + ButtonBase.CommandParameterProperty.AddOwner(typeof(AccountAvatar)); + public static readonly DependencyProperty CommandTargetProperty = + ButtonBase.CommandTargetProperty.AddOwner(typeof(AccountAvatar)); + + public AccountAvatar() + { + InitializeComponent(); + } + + public object Account + { + get { return GetValue(AccountProperty); } + set { SetValue(AccountProperty, value); } + } + + public ICommand Command + { + get { return (ICommand)GetValue(CommandProperty); } + set { SetValue(CommandProperty, value); } + } + + public object CommandParameter + { + get { return GetValue(CommandParameterProperty); } + set { SetValue(CommandParameterProperty, value); } + } + + public IInputElement CommandTarget + { + get { return (IInputElement)GetValue(CommandTargetProperty); } + set { SetValue(CommandTargetProperty, value); } + } + } +} diff --git a/src/GitHub.VisualStudio.UI/UI/Controls/InfoPanel.xaml b/src/GitHub.VisualStudio.UI/UI/Controls/InfoPanel.xaml new file mode 100644 index 0000000000..3c19c8c5a9 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/UI/Controls/InfoPanel.xaml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GitHub.VisualStudio.UI/UI/Controls/InfoPanel.xaml.cs b/src/GitHub.VisualStudio.UI/UI/Controls/InfoPanel.xaml.cs new file mode 100644 index 0000000000..450e09c6b6 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/UI/Controls/InfoPanel.xaml.cs @@ -0,0 +1,114 @@ +using GitHub.UI; +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using GitHub.ViewModels; +using System.ComponentModel; +using GitHub.Services; +using GitHub.Extensions; +using System.Windows.Input; +using GitHub.Primitives; +using GitHub.VisualStudio.Helpers; +using Colors = System.Windows.Media.Colors; + +namespace GitHub.VisualStudio.UI.Controls +{ + public partial class InfoPanel : UserControl, IInfoPanel, INotifyPropertyChanged, INotifyPropertySource + { + static SolidColorBrush WarningColorBrush = new SolidColorBrush(Colors.DarkRed); + static SolidColorBrush InfoColorBrush = new SolidColorBrush(Colors.Black); + + static readonly DependencyProperty MessageProperty = + DependencyProperty.Register(nameof(Message), typeof(string), typeof(InfoPanel), new PropertyMetadata(String.Empty, UpdateMessage)); + + static readonly DependencyProperty MessageTypeProperty = + DependencyProperty.Register(nameof(MessageType), typeof(MessageType), typeof(InfoPanel), new PropertyMetadata(MessageType.Information, UpdateIcon)); + + public string Message + { + get { return (string)GetValue(MessageProperty); } + set { SetValue(MessageProperty, value); } + } + + public MessageType MessageType + { + get { return (MessageType)GetValue(MessageTypeProperty); } + set { SetValue(MessageTypeProperty, value); } + } + + Octicon icon; + public Octicon Icon + { + get { return icon; } + private set { icon = value; RaisePropertyChanged(nameof(Icon)); } + } + + Brush iconColor; + public Brush IconColor + { + get { return iconColor; } + private set { iconColor = value; RaisePropertyChanged(nameof(IconColor)); } + } + + static InfoPanel() + { + WarningColorBrush.Freeze(); + InfoColorBrush.Freeze(); + } + + static IVisualStudioBrowser browser; + static IVisualStudioBrowser Browser + { + get + { + if (browser == null) + browser = Services.GitHubServiceProvider.TryGetService(); + return browser; + } + } + + public InfoPanel() + { + InitializeComponent(); + + DataContext = this; + Icon = Octicon.info; + IconColor = InfoColorBrush; + } + + void OpenHyperlink(object sender, ExecutedRoutedEventArgs e) + { + var url = e.Parameter.ToString(); + + if (!string.IsNullOrEmpty(url)) + Browser.OpenUrl(new Uri(url)); + } + + static void UpdateMessage(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = (InfoPanel)d; + var msg = e.NewValue as string; + control.Visibility = String.IsNullOrEmpty(msg) ? Visibility.Collapsed : Visibility.Visible; + } + + static void UpdateIcon(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = (InfoPanel)d; + control.Icon = (MessageType)e.NewValue == MessageType.Warning ? Octicon.alert : Octicon.info; + control.IconColor = control.Icon == Octicon.alert ? WarningColorBrush : InfoColorBrush; + } + + void Dismiss_Click(object sender, RoutedEventArgs e) + { + SetCurrentValue(MessageProperty, String.Empty); + } + + public event PropertyChangedEventHandler PropertyChanged; + + public void RaisePropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/GitHub.VisualStudio/UI/DrawingExtensions.cs b/src/GitHub.VisualStudio.UI/UI/DrawingExtensions.cs similarity index 100% rename from src/GitHub.VisualStudio/UI/DrawingExtensions.cs rename to src/GitHub.VisualStudio.UI/UI/DrawingExtensions.cs diff --git a/src/GitHub.VisualStudio.UI/UI/Views/GitHubConnectContent.xaml b/src/GitHub.VisualStudio.UI/UI/Views/GitHubConnectContent.xaml new file mode 100644 index 0000000000..66ec45565a --- /dev/null +++ b/src/GitHub.VisualStudio.UI/UI/Views/GitHubConnectContent.xaml @@ -0,0 +1,278 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +