├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── Build.yml │ └── Release.yml ├── .gitignore ├── ExtensionManager.sln ├── ExtensionManager.vsext ├── LICENSE ├── README.md ├── art ├── context-menu.png ├── export.png ├── import.png ├── manage-solution-extensions.png ├── menu_tools.png └── moreInfoUrl_sample.png ├── icon.png └── src ├── Directory.Build.props ├── ExtensionManager.Manifest ├── ExtensionManager.Manifest.csproj ├── IManifest.cs ├── IManifestService.cs ├── Internal │ ├── ManifestService.cs │ ├── ManifestVersion.cs │ ├── Models │ │ ├── ExtensionDto.cs │ │ └── ManifestDto.cs │ ├── ThrowHelper.cs │ └── Versions │ │ ├── V0ManifestVersion.cs │ │ └── V1ManifestVersion.cs └── ServiceCollectionExtensions.cs ├── ExtensionManager.Shared ├── ExtensionManager.Shared.projitems ├── ExtensionManager.Shared.shproj └── System.Runtime.CompilerServices │ ├── CallerArgumentExpressionAttribute.cs │ ├── CompilerFeatureRequiredAttribute.cs │ └── RequiredMemberAttribute.cs ├── ExtensionManager.UI ├── Attached │ └── VSTheme.cs ├── Converters │ ├── AndConverter.cs │ ├── CombineBoolConverterBase.cs │ ├── InvertBoolConverter.cs │ ├── IsNullOrEmptyConverter.cs │ └── IsTypeConverter.cs ├── DialogService.cs ├── ExtensionManager.UI.csproj ├── IDialogService.cs ├── ServiceCollectionExtensions.cs ├── UIMarkupServices.cs ├── Utils │ └── ExtensionEqualityComparer.cs ├── VSExtensionStatus.cs ├── VSExtensionToInstall.cs ├── ViewModels │ ├── Base │ │ ├── DelegateCommand.cs │ │ └── ViewModelBase.cs │ ├── ExportDialogViewModel.cs │ ├── ExtensionViewModel.cs │ ├── InstallDialogViewModel.cs │ ├── InstallExportDialogType.cs │ └── InstallExportDialogViewModel.cs ├── Views │ ├── InstallExportDialogWindow.xaml │ └── InstallExportDialogWindow.xaml.cs ├── Win32 │ ├── NativeMethods.cs │ └── User32.cs └── Worker │ ├── ExportStep.cs │ ├── IExportWorker.cs │ ├── IInstallWorker.cs │ ├── InstallStep.cs │ ├── ProgressStep.cs │ └── ProgressStepExtensions.cs ├── ExtensionManager.VisualStudio.Abstractions ├── Documents │ └── IVSDocuments.cs ├── ExtensionManager.VisualStudio.Abstractions.csproj ├── Extensions │ ├── IVSExtension.cs │ └── IVSExtensions.cs ├── IVSServicesRegistrar.cs ├── MessageBox │ └── IVSMessageBox.cs ├── ServiceCollectionExtensions.cs ├── Solution │ ├── IVSSolution.cs │ ├── IVSSolutionFolder.cs │ ├── IVSSolutionItem.cs │ └── IVSSolutions.cs ├── StatusBar │ └── IVSStatusBar.cs ├── Themes │ └── IVSThemes.cs └── Threads │ └── IVSThreads.cs ├── ExtensionManager.VisualStudio.Adapter.Abstractions ├── ExtensionManager.VisualStudio.Adapter.Abstractions.csproj ├── Extensions │ ├── IVSExtensionManagerAdapter.cs │ ├── IVSExtensionRepositoryAdapter.cs │ ├── IVSInstalledExtensionInfo.cs │ ├── VSExtensionManagerAdapter.cs │ └── VSExtensionRepositoryAdapter.cs └── IVSAdapterServicesFactory.cs ├── ExtensionManager.VisualStudio.Adapter.Generator ├── ExtensionManager.VisualStudio.Adapter.Generator.csproj ├── Internal │ ├── Emitter │ │ ├── ClassEmitter.cs │ │ ├── InterfaceImplementationEmitter.cs │ │ ├── ModuleEmitter.cs │ │ └── PropertyEmitter.cs │ ├── GeneratorContext.cs │ ├── GeneratorReflector.cs │ ├── ITypeGenerator.cs │ └── Utils │ │ ├── LinqExtensions.cs │ │ ├── ReflectionEmitExtensions.cs │ │ └── ReflectionExtensions.cs ├── Types │ ├── AdapterServicesFactoryGenerator.cs │ └── Extensions │ │ ├── ExtensionManagerAdapterGenerator.cs │ │ ├── ExtensionRepositoryAdapterGenerator.cs │ │ ├── GalleryExtensionGenerator.cs │ │ └── InstalledExtensionInfoGenerator.cs └── VSAdapterServicesFactoryGeneratorBase.cs ├── ExtensionManager.VisualStudio.Adapter.md ├── ExtensionManager.VisualStudio.Shared ├── Documents │ └── VSDocuments.cs ├── ExtensionManager.VisualStudio.Shared.projitems ├── ExtensionManager.VisualStudio.Shared.shproj ├── Extensions │ └── VSExtensions.cs ├── MessageBox │ └── VSMessageBox.cs ├── Solution │ ├── VSSolution.cs │ ├── VSSolutionFolder.cs │ ├── VSSolutionItem.cs │ └── VSSolutions.cs ├── StatusBar │ └── VSStatusBar.cs ├── Themes │ └── VSThemes.cs ├── Threads │ └── VSThreads.cs ├── VSAdapterServicesFactoryGenerator.cs └── VSServicesRegistrar.cs ├── ExtensionManager.VisualStudio.md ├── ExtensionManager.Vsix.Shared ├── ExtensionManager.Vsix.Shared.projitems ├── ExtensionManager.Vsix.Shared.shproj ├── ExtensionManagerPackage.cs ├── Properties │ └── AssemblyInfo.cs └── ThisVsixInfo.cs ├── ExtensionManager.Vsix.VS2017 ├── ExtensionManager.Vsix.VS2017.csproj ├── VsComandTable.cs ├── VsComandTable.vsct ├── source.extension.cs └── source.extension.vsixmanifest ├── ExtensionManager.Vsix.VS2019 ├── ExtensionManager.Vsix.VS2019.csproj ├── VsComandTable.cs ├── VsComandTable.vsct ├── source.extension.cs └── source.extension.vsixmanifest ├── ExtensionManager.Vsix.VS2022 ├── ExtensionManager.Vsix.VS2022.csproj ├── VsComandTable.cs ├── VsComandTable.vsct ├── source.extension.cs └── source.extension.vsixmanifest ├── ExtensionManager.Vsix.md ├── ExtensionManager.Vsix.props ├── ExtensionManager.md └── ExtensionManager ├── ExtensionManager.csproj ├── FeatureExecutor.cs ├── Features ├── Export │ ├── ExportFeature.cs │ ├── ExportFeatureBase.cs │ └── ExportSolutionFeature.cs ├── Install │ ├── InstallFeature.cs │ ├── InstallFeatureBase.cs │ └── InstallForSolutionFeature.cs └── VisualStudioExtensions.cs ├── IFeature.cs ├── IFeatureExecutor.cs ├── IThisVsixInfo.cs ├── Installation ├── DownloadProgres.cs ├── DownloadResult.cs ├── ExtensionDownloader.cs ├── ExtensionInstaller.cs └── IExtensionInstaller.cs ├── ServiceCollectionExtensions.cs └── Utils └── ExtensionEqualityComparer.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/Build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | push: 7 | branches: 8 | - master 9 | 10 | workflow_call: 11 | outputs: 12 | version: 13 | value: ${{ jobs.build.outputs.version }} 14 | 15 | jobs: 16 | build: 17 | strategy: 18 | matrix: 19 | vsVersion: ["2017", "2019", "2022"] 20 | 21 | runs-on: windows-2022 22 | 23 | outputs: 24 | version: ${{ steps.version.outputs.version }} 25 | 26 | steps: 27 | - name: Checkup code 28 | uses: actions/checkout@v4.1.1 29 | with: 30 | fetch-depth: 0 31 | 32 | - name: Setup NuGet 33 | uses: nuget/setup-nuget@v2 34 | 35 | - name: Setup MSBuild 36 | uses: microsoft/setup-msbuild@v2 37 | 38 | - name: Setup .NET Core SDK 39 | uses: actions/setup-dotnet@v4 40 | with: 41 | dotnet-version: '8.0.x' 42 | 43 | - name: Search and increment last version 44 | id: version 45 | run: | 46 | $latestTag = git tag --list 'v*' | Where-Object { $_ -match '^v[0-9]+\.[0-9]+\.[0-9]+$' } | Sort-Object { [Version]($_.TrimStart('v')) } | Select-Object -Last 1 47 | $oldVersion = $latestTag.TrimStart('v') 48 | $versionParts = $oldVersion -split '\.' 49 | $versionParts[2] = [int]$versionParts[2] + 1 50 | $newVersion = "$($versionParts -join '.')" 51 | echo version=$newVersion >> $env:GITHUB_OUTPUT 52 | 53 | - name: Set manifest versions 54 | shell: pwsh 55 | run: | 56 | $paths = @( 57 | "src/ExtensionManager.Vsix.VS${{ matrix.vsVersion }}/source.extension.vsixmanifest", 58 | "src/ExtensionManager.Vsix.VS${{ matrix.vsVersion }}/source.extension.cs" 59 | ) 60 | 61 | foreach ($path in $paths) { 62 | (Get-Content -Path $path) -Replace '9.9.9999', '${{ steps.version.outputs.version }}' | Set-Content -Path $path 63 | } 64 | 65 | - name: Build library projects 66 | shell: pwsh 67 | run: | 68 | $projects = Get-ChildItem -Recurse -Filter *.csproj -Exclude *.Vsix.*.csproj | Select-Object -ExpandProperty FullName 69 | 70 | foreach ($project in $projects) { 71 | dotnet build "$project" -c Release 72 | } 73 | 74 | - name: Build vsix project 75 | shell: pwsh 76 | run: | 77 | $project = "src/ExtensionManager.Vsix.VS${{ matrix.vsVersion }}/ExtensionManager.Vsix.VS${{ matrix.vsVersion }}.csproj" 78 | 79 | nuget restore "$project" 80 | msbuild /p:BuildProjectReferences=False /p:RestorePackages=False /p:Configuration=Release /p:DeployExtension=False /p:ZipPackageCompressionLevel=normal /v:n "$project" 81 | 82 | - name: Upload artifact 83 | uses: actions/upload-artifact@v4.3.1 84 | with: 85 | name: ExtensionManager${{ matrix.vsVersion }}.vsix 86 | path: src/ExtensionManager.Vsix.VS${{ matrix.vsVersion }}/bin/Release/ExtensionManager${{ matrix.vsVersion }}.vsix 87 | if-no-files-found: error 88 | compression-level: 0 89 | -------------------------------------------------------------------------------- /.github/workflows/Release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: write 8 | 9 | jobs: 10 | build: 11 | uses: ./.github/workflows/Build.yml 12 | 13 | release: 14 | needs: build 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkup code 19 | uses: actions/checkout@v4.1.1 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Download VSIX 2017 24 | uses: actions/download-artifact@v4.1.7 25 | with: 26 | name: ExtensionManager2017.vsix 27 | 28 | - name: Download VSIX 2019 29 | uses: actions/download-artifact@v4.1.7 30 | with: 31 | name: ExtensionManager2019.vsix 32 | 33 | - name: Download VSIX 2022 34 | uses: actions/download-artifact@v4.1.7 35 | with: 36 | name: ExtensionManager2022.vsix 37 | 38 | - name: Unzip VSIX Archives 39 | shell: pwsh 40 | run: | 41 | Get-ChildItem . -Filter *.zip | ForEach-Object { 42 | Expand-Archive -LiteralPath $_.FullName -DestinationPath . 43 | } 44 | 45 | - name: Create version tag 46 | run: | 47 | git config user.name "GitHub Action" 48 | git config user.email "<>" 49 | git tag v${{ needs.build.outputs.version }} 50 | git push origin v${{ needs.build.outputs.version }} 51 | 52 | - name: Create Release 53 | env: 54 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | run: | 56 | gh release create v${{ needs.build.outputs.version }} 57 | 58 | - name: Upload Release Binaries 59 | env: 60 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | run: | 62 | gh release upload v${{ needs.build.outputs.version }} "ExtensionManager2017.vsix" "ExtensionManager2019.vsix" "ExtensionManager2022.vsix" 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.pubxml 8 | *.userosscache 9 | *.sln.docstates 10 | 11 | # User-specific files (MonoDevelop/Xamarin Studio) 12 | *.userprefs 13 | 14 | # Build results 15 | [Dd]ebug/ 16 | [Dd]ebugPublic/ 17 | [Rr]elease/ 18 | [Rr]eleases/ 19 | x64/ 20 | x86/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | [Ll]og/ 25 | 26 | # Visual Studio 2015 cache/options directory 27 | .vs/ 28 | # Uncomment if you have tasks that create the project's static files in wwwroot 29 | #wwwroot/ 30 | 31 | # MSTest test Results 32 | [Tt]est[Rr]esult*/ 33 | [Bb]uild[Ll]og.* 34 | 35 | # NUNIT 36 | *.VisualState.xml 37 | TestResult.xml 38 | 39 | # Build Results of an ATL Project 40 | [Dd]ebugPS/ 41 | [Rr]eleasePS/ 42 | dlldata.c 43 | 44 | # DNX 45 | project.lock.json 46 | project.fragment.lock.json 47 | artifacts/ 48 | 49 | *_i.c 50 | *_p.c 51 | *_i.h 52 | *.ilk 53 | *.meta 54 | *.obj 55 | *.pch 56 | *.pdb 57 | *.pgc 58 | *.pgd 59 | *.rsp 60 | *.sbr 61 | *.tlb 62 | *.tli 63 | *.tlh 64 | *.tmp 65 | *.tmp_proj 66 | *.log 67 | *.vspscc 68 | *.vssscc 69 | .builds 70 | *.pidb 71 | *.svclog 72 | *.scc 73 | 74 | # Chutzpah Test files 75 | _Chutzpah* 76 | 77 | # Visual C++ cache files 78 | ipch/ 79 | *.aps 80 | *.ncb 81 | *.opendb 82 | *.opensdf 83 | *.sdf 84 | *.cachefile 85 | *.VC.db 86 | *.VC.VC.opendb 87 | 88 | # Visual Studio profiler 89 | *.psess 90 | *.vsp 91 | *.vspx 92 | *.sap 93 | 94 | # TFS 2012 Local Workspace 95 | $tf/ 96 | 97 | # Guidance Automation Toolkit 98 | *.gpState 99 | 100 | # ReSharper is a .NET coding add-in 101 | _ReSharper*/ 102 | *.[Rr]e[Ss]harper 103 | *.DotSettings.user 104 | 105 | # JustCode is a .NET coding add-in 106 | .JustCode 107 | 108 | # TeamCity is a build add-in 109 | _TeamCity* 110 | 111 | # DotCover is a Code Coverage Tool 112 | *.dotCover 113 | 114 | # NCrunch 115 | _NCrunch_* 116 | .*crunch*.local.xml 117 | nCrunchTemp_* 118 | 119 | # MightyMoose 120 | *.mm.* 121 | AutoTest.Net/ 122 | 123 | # Web workbench (sass) 124 | .sass-cache/ 125 | 126 | # Installshield output folder 127 | [Ee]xpress/ 128 | 129 | # DocProject is a documentation generator add-in 130 | DocProject/buildhelp/ 131 | DocProject/Help/*.HxT 132 | DocProject/Help/*.HxC 133 | DocProject/Help/*.hhc 134 | DocProject/Help/*.hhk 135 | DocProject/Help/*.hhp 136 | DocProject/Help/Html2 137 | DocProject/Help/html 138 | 139 | # Click-Once directory 140 | publish/ 141 | 142 | # Publish Web Output 143 | *.[Pp]ublish.xml 144 | *.azurePubxml 145 | # TODO: Comment the next line if you want to checkin your web deploy settings 146 | # but database connection strings (with potential passwords) will be unencrypted 147 | #*.pubxml 148 | *.publishproj 149 | 150 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 151 | # checkin your Azure Web App publish settings, but sensitive information contained 152 | # in these scripts will be unencrypted 153 | PublishScripts/ 154 | 155 | # NuGet Packages 156 | *.nupkg 157 | # The packages folder can be ignored because of Package Restore 158 | **/packages/* 159 | # except build/, which is used as an MSBuild target. 160 | !**/packages/build/ 161 | # Uncomment if necessary however generally it will be regenerated when needed 162 | #!**/packages/repositories.config 163 | # NuGet v3's project.json files produces more ignoreable files 164 | *.nuget.props 165 | *.nuget.targets 166 | 167 | # Microsoft Azure Build Output 168 | csx/ 169 | *.build.csdef 170 | 171 | # Microsoft Azure Emulator 172 | ecf/ 173 | rcf/ 174 | 175 | # Windows Store app package directories and files 176 | AppPackages/ 177 | BundleArtifacts/ 178 | Package.StoreAssociation.xml 179 | _pkginfo.txt 180 | 181 | # Visual Studio cache files 182 | # files ending in .cache can be ignored 183 | *.[Cc]ache 184 | # but keep track of directories ending in .cache 185 | !*.[Cc]ache/ 186 | 187 | # Others 188 | ClientBin/ 189 | ~$* 190 | *~ 191 | *.dbmdl 192 | *.dbproj.schemaview 193 | *.jfm 194 | *.pfx 195 | *.publishsettings 196 | node_modules/ 197 | orleans.codegen.cs 198 | 199 | # Since there are multiple workflows, uncomment next line to ignore bower_components 200 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 201 | #bower_components/ 202 | 203 | # RIA/Silverlight projects 204 | Generated_Code/ 205 | 206 | # Backup & report files from converting an old project file 207 | # to a newer Visual Studio version. Backup files are not needed, 208 | # because we have git ;-) 209 | _UpgradeReport_Files/ 210 | Backup*/ 211 | UpgradeLog*.XML 212 | UpgradeLog*.htm 213 | 214 | # SQL Server files 215 | *.mdf 216 | *.ldf 217 | 218 | # Business Intelligence projects 219 | *.rdl.data 220 | *.bim.layout 221 | *.bim_*.settings 222 | 223 | # Microsoft Fakes 224 | FakesAssemblies/ 225 | 226 | # GhostDoc plugin setting file 227 | *.GhostDoc.xml 228 | 229 | # Node.js Tools for Visual Studio 230 | .ntvs_analysis.dat 231 | 232 | # Visual Studio 6 build log 233 | *.plg 234 | 235 | # Visual Studio 6 workspace options file 236 | *.opt 237 | 238 | # Visual Studio LightSwitch build output 239 | **/*.HTMLClient/GeneratedArtifacts 240 | **/*.DesktopClient/GeneratedArtifacts 241 | **/*.DesktopClient/ModelManifest.xml 242 | **/*.Server/GeneratedArtifacts 243 | **/*.Server/ModelManifest.xml 244 | _Pvt_Extensions 245 | 246 | # Paket dependency manager 247 | .paket/paket.exe 248 | paket-files/ 249 | 250 | # FAKE - F# Make 251 | .fake/ 252 | 253 | # JetBrains Rider 254 | .idea/ 255 | *.sln.iml 256 | 257 | # CodeRush 258 | .cr/ 259 | 260 | # Python Tools for Visual Studio (PTVS) 261 | __pycache__/ 262 | *.pyc -------------------------------------------------------------------------------- /ExtensionManager.vsext: -------------------------------------------------------------------------------- 1 | { 2 | "id": "b14d676e-2878-42b2-96ba-602b47768a15", 3 | "name": "My Visual Studio extensions", 4 | "description": "A collection of my Visual Studio extensions", 5 | "version": "1.0", 6 | "extensions": [ 7 | { 8 | "name": "VSIX Synchronizer", 9 | "vsixId": "751759cc-53b5-45f6-8d75-43392a1dd89c", 10 | "moreInfoUrl": "https://marketplace.visualstudio.com/items?itemName=MadsKristensen.VsixSynchronizer64", 11 | "downloadUrl": "https://madskristensen.gallery.vsassets.io:443/_apis/public/gallery/publisher/MadsKristensen/extension/VsixSynchronizer64/1.0.27/assetbyname/Microsoft.VisualStudio.Ide.Payload?redirect=true&update=true" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Mads Kristensen 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Import/Export Visual Studio extensions 3 | 4 | [![](https://img.shields.io/github/actions/workflow/status/loop8ack/ExtensionPackTools/Build.yml?branch=master&label=Build%20Status)](https://github.com/loop8ack/ExtensionPackTools/actions/workflows/Build.yml) 5 | [![](https://img.shields.io/github/actions/workflow/status/loop8ack/ExtensionPackTools/Release.yml?branch=master&label=Latest%20Release)](https://github.com/loop8ack/ExtensionPackTools/releases/latest) 6 | 7 | Download the extension from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=Loop8ack.ExtensionManager2022). 8 | 9 | -------------------------------------- 10 | 11 | This extension allows you to export a list of extensions and importing them back into any instance of Visual Studio. 12 | 13 | ![Tools menu](art/menu_tools.png) 14 | 15 | **Figure 1.** The **Export Extensions** and **Import Extensions** menu commands. 16 | 17 | ## Export 18 | 19 | The **Export Extensions** dialog box appears that lets you select which extensions you wish to export. 20 | 21 | Check the boxes for the extension(s) you wish to export, and then click **Export** to perform the operation. 22 | 23 | Click the **Select/deselect all** to toggle back and forth between selecting or deselecting all the extensions in the list. 24 | 25 | ![Export](art/export.png) 26 | 27 | **Figure 2.** The **Export Extensions** dialog box. 28 | 29 | The output is a JSON file with an `.vsext` file extension looking like this: 30 | 31 | ```json 32 | { 33 | "id": "49481cf2-0f02-462e-b8b7-9ecc53fee721", 34 | "name": "My Visual Studio extensions", 35 | "description": "A collection of my Visual Studio extensions", 36 | "version": "1.0", 37 | "extensions": [ 38 | { 39 | "name": "Add Multiple Projects To Solution", 40 | "vsixId": "2ed01419-2b11-4128-a2ca-0adfa0fc7498", 41 | "moreInfoUrl": "https://marketplace.visualstudio.com/items?itemName=MaciejGudanowicz.AddMultipleProjectsToSolution", 42 | "downloadUrl": "https://maciejgudanowicz.gallery.vsassets.io:443/_apis/public/gallery/publisher/MaciejGudanowicz/extension/AddMultipleProjectsToSolution/1.2.0/assetbyname/Microsoft.VisualStudio.Ide.Payload?redirect=true&update=true" 43 | }, 44 | { 45 | "name": "Add New File", 46 | "vsixId": "2E78AA18-E864-4FBB-B8C8-6186FC865DB3", 47 | "moreInfoUrl": "https://marketplace.visualstudio.com/items?itemName=MadsKristensen.AddNewFile", 48 | "downloadUrl": "https://madskristensen.gallery.vsassets.io:443/_apis/public/gallery/publisher/MadsKristensen/extension/AddNewFile/3.5.160/assetbyname/Microsoft.VisualStudio.Ide.Payload?redirect=true&update=true" 49 | }, 50 | { 51 | "name": "Advanced Installer for Visual Studio 2019", 52 | "vsixId": "Caphyon.AdvancedInstaller.5a62525e-63ff-4f65-8949-c5e3f35bf9a8", 53 | "moreInfoUrl": "https://marketplace.visualstudio.com/items?itemName=caphyon.AdvancedInstallerforVisualStudio2019", 54 | "downloadUrl": "https://caphyon.gallery.vsassets.io:443/_apis/public/gallery/publisher/caphyon/extension/AdvancedInstallerforVisualStudio2019/19.0/assetbyname/Microsoft.VisualStudio.Ide.Payload?redirect=true&update=true" 55 | } 56 | ] 57 | } 58 | ``` 59 | 60 | **Listing 1.** The contents of the new `.vsext` export. 61 | 62 | ### New Export Fields 63 | 64 | Of note are new entries: `moreInfoUrl` and `downloadUrl` for each extension. These are now exported along with the `vsixId` and `name` fields. 65 | 66 | #### The `moreInfoUrl` field 67 | 68 | The `moreInfoUrl` field points to the Visual Studio Marketplace page of the extension. If you open this URL in a Web browser, then the Visual Studio marketplace will show that extension's page: 69 | 70 | ![More Info Url Sample](art/moreInfoUrl_sample.png) 71 | 72 | **Figure 3.** Google Chrome opened to the URL in the `moreInfoUrl` field for the `Windows App SDK (Experimental)` extension. 73 | 74 | #### The `downloadUrl` field 75 | 76 | The `downloadUrl` field points to the URL that a `HTTP GET` request can be issued to in order to obtain the `.vsix` file of the extension itself. 77 | 78 | ### Example Use Case for New Export Fields 79 | 80 | The file can be parsed by a custom script you write. The use case is, e.g., say a Sysadmin at a large organization needs to install the same suite of extensions into all the Visual Studio 2019 instances in a computer lab. 81 | 82 | For such a use case, the procedure is as follows: 83 | 84 | 1. Configure a 'reference' workstation's copy of Visual Studio with the set of extensions you want. 85 | 2. Also install this extension. 86 | 3. Do an `Export Extensions` operation from the menu command. 87 | 4. Save the `.vsext` file to a common location where your script can see it. 88 | 5. Download and install the extensions, using your script, across all your computer-lab machines. 89 | 90 | ## Import 91 | Clicking the import button prompts you to select a `.vsext` file. Doing that will present you with the **Import Extensions** dialog that lists all the extensions found in the `.vsext` file you selected. 92 | 93 | ![Import](art/import.png) 94 | 95 | **Figure 4.** The **Import Extensions** dialog box. 96 | 97 | Before showing the list it will verify that the extensions exist on the Marketplace and that can take a few seconds. 98 | 99 | Any extensions in the import file that are already installed in Visual Studio will be grayed out. 100 | 101 | Clicking the **Import** button in the dialog will start the VSIX Installer in a separate process and you can follow the normal install flow from there. 102 | 103 | ## Manage Solution Extensions 104 | This allows you to specify which extensions needed to work on any given solution. When a developer opens the solution and doesn't have one or more of the extensions installed, they are prompted to install them. 105 | 106 | Right-click the solution to manage the extensions. 107 | 108 | ![Context menu](art/context-menu.png) 109 | 110 | **Figure 5.** Context menu for the Solution level of **Solution Explorer**. 111 | 112 | This will show this dialog where you can pick wich of your extensions to associate with the solution. 113 | 114 | ![Manage solution extensions](art/manage-solution-extensions.png) 115 | 116 | **Figure 6.** The **Manage Solution Extensions** dialog box. 117 | 118 | To create a `.vsext` file containing the checked extensions in a location on the local disk that is next to the Solution file (`.sln`), check the extensions you want, and then click the **Select** button. 119 | 120 | You have the option to commit the generated .vsext file to souce control. This is highly recommended. 121 | 122 | ## Project Takeover 123 | 124 | This project has been taken over by [Loop8ack](https://github.com/loop8ack), the original author was [Mads Kristensen](https://github.com/madskristensen). 125 | 126 | I have assumed ownership and responsibility for the further development and maintenance of this project. As the new maintainer, I will be actively working on improving and adding new features to the project. 127 | 128 | Please feel free to [create an Issue](https://github.com/loop8ack/ExtensionPackTools/issues) for any questions, bug reports, or feature requests you may have. Your feedback and contributions are highly appreciated as I continue with the development of this project. 129 | 130 | Thank you! 131 | 132 | ## License 133 | [Apache 2.0](LICENSE) 134 | -------------------------------------------------------------------------------- /art/context-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loop8ack/ExtensionPackTools/edaa5cb7a5c2d8af032f82ffae19eaebdfe4c7e7/art/context-menu.png -------------------------------------------------------------------------------- /art/export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loop8ack/ExtensionPackTools/edaa5cb7a5c2d8af032f82ffae19eaebdfe4c7e7/art/export.png -------------------------------------------------------------------------------- /art/import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loop8ack/ExtensionPackTools/edaa5cb7a5c2d8af032f82ffae19eaebdfe4c7e7/art/import.png -------------------------------------------------------------------------------- /art/manage-solution-extensions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loop8ack/ExtensionPackTools/edaa5cb7a5c2d8af032f82ffae19eaebdfe4c7e7/art/manage-solution-extensions.png -------------------------------------------------------------------------------- /art/menu_tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loop8ack/ExtensionPackTools/edaa5cb7a5c2d8af032f82ffae19eaebdfe4c7e7/art/menu_tools.png -------------------------------------------------------------------------------- /art/moreInfoUrl_sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loop8ack/ExtensionPackTools/edaa5cb7a5c2d8af032f82ffae19eaebdfe4c7e7/art/moreInfoUrl_sample.png -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loop8ack/ExtensionPackTools/edaa5cb7a5c2d8af032f82ffae19eaebdfe4c7e7/icon.png -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net472 5 | preview 6 | enable 7 | true 8 | enable 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/ExtensionManager.Manifest/ExtensionManager.Manifest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/ExtensionManager.Manifest/IManifest.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.Extensions; 2 | 3 | namespace ExtensionManager.Manifest; 4 | 5 | public interface IManifest 6 | { 7 | Guid Id { get; } 8 | string? Name { get; set; } 9 | string? Description { get; set; } 10 | IList Extensions { get; } 11 | } 12 | -------------------------------------------------------------------------------- /src/ExtensionManager.Manifest/IManifestService.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.Manifest; 2 | 3 | public interface IManifestService 4 | { 5 | IManifest CreateNew(); 6 | Task ReadAsync(string filePath); 7 | Task WriteAsync(string filePath, IManifest manifest, CancellationToken cancellationToken); 8 | } 9 | -------------------------------------------------------------------------------- /src/ExtensionManager.Manifest/Internal/ManifestService.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.Manifest.Internal.Models; 2 | using ExtensionManager.VisualStudio.Extensions; 3 | 4 | namespace ExtensionManager.Manifest.Internal; 5 | 6 | internal sealed class ManifestService : IManifestService 7 | { 8 | public IManifest CreateNew() 9 | { 10 | return new ManifestDto() 11 | { 12 | Id = Guid.NewGuid(), 13 | Name = "My Visual Studio extensions", 14 | Description = "A collection of my Visual Studio extensions", 15 | Extensions = new List() 16 | }; 17 | } 18 | 19 | public async Task ReadAsync(string filePath) 20 | { 21 | using var stream = File.OpenRead(filePath); 22 | 23 | try 24 | { 25 | return await ManifestVersion.Latest.ReadAsync(stream).ConfigureAwait(false); 26 | } 27 | catch 28 | { 29 | stream.Position = 0; 30 | 31 | var foundVersion = await ManifestVersion.FindAsync(stream).ConfigureAwait(false); 32 | 33 | if (foundVersion == ManifestVersion.Latest) 34 | throw; 35 | 36 | stream.Position = 0; 37 | 38 | return await foundVersion.ReadAsync(stream).ConfigureAwait(false); 39 | } 40 | } 41 | 42 | public async Task WriteAsync(string filePath, IManifest manifest, CancellationToken cancellationToken) 43 | { 44 | var directoryPath = Path.GetDirectoryName(filePath); 45 | 46 | if (string.IsNullOrWhiteSpace(directoryPath)) 47 | directoryPath = "."; 48 | 49 | Directory.CreateDirectory(directoryPath); 50 | 51 | using var stream = File.Create(filePath); 52 | 53 | await ManifestVersion.Latest.WriteAsync(stream, manifest, cancellationToken); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/ExtensionManager.Manifest/Internal/ManifestVersion.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | using ExtensionManager.Manifest.Internal.Versions; 4 | 5 | namespace ExtensionManager.Manifest.Internal; 6 | 7 | internal abstract class ManifestVersion 8 | { 9 | public const string VersionPropertyName = "version"; 10 | 11 | private static readonly SortedList _versions; 12 | 13 | public static ManifestVersion First { get; } 14 | public static ManifestVersion Latest { get; } 15 | 16 | static ManifestVersion() 17 | { 18 | _versions = new SortedList(); 19 | 20 | AddVersion(_versions); 21 | AddVersion(_versions); 22 | 23 | First = _versions.First().Value; 24 | Latest = _versions.Last().Value; 25 | 26 | static void AddVersion(SortedList versionsList) 27 | where TVersion : ManifestVersion, new() 28 | { 29 | TVersion version = new(); 30 | 31 | versionsList.Add(version.Version, version); 32 | } 33 | } 34 | 35 | public static async Task FindAsync(Stream stream) 36 | { 37 | var document = await JsonDocument.ParseAsync(stream); 38 | 39 | if (!document.RootElement.TryGetProperty(VersionPropertyName, out var versionProperty)) 40 | return First; 41 | 42 | var value = versionProperty.GetString(); 43 | 44 | if (!Version.TryParse(value, out var version)) 45 | return First; 46 | 47 | ManifestVersion? foundVersion = null; 48 | 49 | foreach (var item in _versions.Values) 50 | { 51 | if (version >= item.Version) 52 | foundVersion = item; 53 | else 54 | break; 55 | } 56 | 57 | return foundVersion 58 | ?? throw new InvalidOperationException($"Unknown manifest file version: {version}"); 59 | } 60 | 61 | public Version Version { get; } 62 | 63 | protected ManifestVersion(Version version) 64 | { 65 | Version = version; 66 | } 67 | 68 | public abstract Task ReadAsync(Stream stream); 69 | public abstract Task WriteAsync(Stream stream, IManifest manifest, CancellationToken cancellationToken); 70 | 71 | protected Exception CreateVersionNotSupportedException() 72 | => ThrowHelper.CreateManifestVersionNotSupportedException(Version); 73 | } 74 | -------------------------------------------------------------------------------- /src/ExtensionManager.Manifest/Internal/Models/ExtensionDto.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.Extensions; 2 | 3 | namespace ExtensionManager.Manifest.Internal.Models; 4 | 5 | internal sealed class ExtensionDto : IVSExtension 6 | { 7 | public required string Id { get; init; } 8 | public required string? Name { get; init; } 9 | public required string? MoreInfoURL { get; init; } 10 | public required string? DownloadUrl { get; init; } 11 | } 12 | -------------------------------------------------------------------------------- /src/ExtensionManager.Manifest/Internal/Models/ManifestDto.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.Extensions; 2 | 3 | namespace ExtensionManager.Manifest.Internal.Models; 4 | 5 | internal sealed class ManifestDto : IManifest 6 | { 7 | public required Guid Id { get; init; } 8 | public required string? Name { get; set; } 9 | public required string? Description { get; set; } 10 | public required IList Extensions { get; init; } 11 | } 12 | -------------------------------------------------------------------------------- /src/ExtensionManager.Manifest/Internal/ThrowHelper.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.Manifest.Internal; 2 | 3 | internal static class ThrowHelper 4 | { 5 | public static Exception CreateCannotReadManifestException() 6 | => new InvalidOperationException($"Cannot read manifest file"); 7 | 8 | public static Exception CreateExtensionDataHasNoVsixIdException() 9 | => new InvalidOperationException($"Extension data in json file has no vsix id"); 10 | 11 | public static Exception CreateManifestVersionNotSupportedException(Version version) 12 | => new NotSupportedException($"Writing file version {version} is no longer supported."); 13 | } 14 | -------------------------------------------------------------------------------- /src/ExtensionManager.Manifest/Internal/Versions/V0ManifestVersion.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | using ExtensionManager.Manifest.Internal.Models; 5 | using ExtensionManager.VisualStudio.Extensions; 6 | 7 | namespace ExtensionManager.Manifest.Internal.Versions; 8 | 9 | internal class V0ManifestVersion : ManifestVersion 10 | { 11 | public V0ManifestVersion() 12 | : base(new(0, 0)) 13 | { 14 | } 15 | 16 | public override async Task ReadAsync(Stream stream) 17 | { 18 | var data = await JsonSerializer 19 | .DeserializeAsync(stream, (JsonSerializerOptions?)null) 20 | .ConfigureAwait(false); 21 | 22 | if (data is null) 23 | throw ThrowHelper.CreateCannotReadManifestException(); 24 | 25 | return CreateManifestDto(data); 26 | 27 | static ManifestDto CreateManifestDto(JsonManifest data) 28 | { 29 | var extensions = new List(); 30 | 31 | if (data.Extensions?.Optional is not null) 32 | { 33 | foreach (var ext in data.Extensions.Optional) 34 | extensions.Add(CreateExtensionDto(ext)); 35 | } 36 | 37 | if (data.Extensions?.Mandatory is not null) 38 | { 39 | foreach (var ext in data.Extensions.Mandatory) 40 | extensions.Add(CreateExtensionDto(ext)); 41 | } 42 | 43 | return new ManifestDto() 44 | { 45 | Id = Guid.NewGuid(), 46 | Name = "Legacy file", 47 | Description = "Legacy file", 48 | Extensions = extensions, 49 | }; 50 | } 51 | 52 | static ExtensionDto CreateExtensionDto(JsonExtension data) 53 | { 54 | return new ExtensionDto() 55 | { 56 | Id = data.ID ?? throw ThrowHelper.CreateExtensionDataHasNoVsixIdException(), 57 | Name = data.Name, 58 | MoreInfoURL = data.MoreInfoURL, 59 | DownloadUrl = null, 60 | }; 61 | } 62 | } 63 | 64 | public override Task WriteAsync(Stream stream, IManifest manifest, CancellationToken cancellationToken) 65 | => throw CreateVersionNotSupportedException(); 66 | } 67 | 68 | file sealed class JsonManifest 69 | { 70 | [JsonPropertyName("extensions")] 71 | public JsonManifestExtensions? Extensions { get; set; } 72 | } 73 | 74 | file sealed class JsonManifestExtensions 75 | { 76 | [JsonPropertyName("mandatory")] 77 | public IEnumerable? Mandatory { get; set; } 78 | 79 | [JsonPropertyName("optional")] 80 | public IEnumerable? Optional { get; set; } 81 | } 82 | 83 | file sealed class JsonExtension 84 | { 85 | [JsonPropertyName("productId")] 86 | public string? ID { get; set; } 87 | 88 | [JsonPropertyName("name")] 89 | public string? Name { get; set; } 90 | 91 | [JsonPropertyName("link")] 92 | public string? MoreInfoURL { get; set; } 93 | } 94 | -------------------------------------------------------------------------------- /src/ExtensionManager.Manifest/Internal/Versions/V1ManifestVersion.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | using ExtensionManager.Manifest.Internal.Models; 5 | using ExtensionManager.VisualStudio.Extensions; 6 | 7 | namespace ExtensionManager.Manifest.Internal.Versions; 8 | 9 | internal class V1ManifestVersion : ManifestVersion 10 | { 11 | public V1ManifestVersion() 12 | : base(new(1, 0)) 13 | { 14 | } 15 | 16 | public override async Task ReadAsync(Stream stream) 17 | { 18 | var data = await JsonSerializer 19 | .DeserializeAsync(stream, (JsonSerializerOptions?)null) 20 | .ConfigureAwait(false); 21 | 22 | if (data is null) 23 | throw ThrowHelper.CreateCannotReadManifestException(); 24 | 25 | return CreateManifestDto(data); 26 | 27 | static ManifestDto CreateManifestDto(JsonManifest data) 28 | { 29 | var extensions = new List(); 30 | 31 | if (data.Extensions is not null) 32 | { 33 | foreach (var ext in data.Extensions) 34 | extensions.Add(CreateExtensionDto(ext)); 35 | } 36 | 37 | return new ManifestDto() 38 | { 39 | Id = data.Id, 40 | Name = data.Name, 41 | Description = data.Description, 42 | Extensions = extensions, 43 | }; 44 | } 45 | 46 | static ExtensionDto CreateExtensionDto(JsonExtension data) 47 | { 48 | return new ExtensionDto() 49 | { 50 | Id = data.ID ?? throw ThrowHelper.CreateExtensionDataHasNoVsixIdException(), 51 | Name = data.Name, 52 | MoreInfoURL = data.MoreInfoURL, 53 | DownloadUrl = data.DownloadUrl, 54 | }; 55 | } 56 | } 57 | 58 | public override async Task WriteAsync(Stream stream, IManifest manifest, CancellationToken cancellationToken) 59 | { 60 | var data = new JsonManifest(manifest); 61 | 62 | var options = new JsonSerializerOptions() 63 | { 64 | WriteIndented = true, 65 | }; 66 | 67 | await JsonSerializer.SerializeAsync(stream, data, options, cancellationToken); 68 | } 69 | } 70 | 71 | file sealed class JsonManifest 72 | { 73 | [JsonPropertyName("id")] 74 | public Guid Id { get; set; } 75 | 76 | [JsonPropertyName(ManifestVersion.VersionPropertyName)] 77 | public Version Version => new(1, 0); 78 | 79 | [JsonPropertyName("name")] 80 | public string? Name { get; set; } 81 | 82 | [JsonPropertyName("description")] 83 | public string? Description { get; set; } 84 | 85 | [JsonPropertyName("extensions")] 86 | public List? Extensions { get; set; } 87 | 88 | [JsonConstructor] 89 | public JsonManifest() { } 90 | 91 | public JsonManifest(IManifest dto) 92 | { 93 | Id = dto.Id; 94 | Name = dto.Name; 95 | Description = dto.Description; 96 | Extensions = new List(); 97 | 98 | foreach (var ext in dto.Extensions) 99 | Extensions.Add(new(ext)); 100 | } 101 | } 102 | 103 | file sealed class JsonExtension 104 | { 105 | [JsonPropertyName("vsixId")] 106 | public string? ID { get; set; } 107 | 108 | [JsonPropertyName("name")] 109 | public string? Name { get; set; } 110 | 111 | [JsonPropertyName("moreInfoUrl")] 112 | public string? MoreInfoURL { get; set; } 113 | 114 | [JsonPropertyName("downloadUrl")] 115 | public string? DownloadUrl { get; set; } 116 | 117 | [JsonConstructor] 118 | public JsonExtension() { } 119 | 120 | public JsonExtension(IVSExtension dto) 121 | { 122 | ID = dto.Id; 123 | Name = dto.Name; 124 | MoreInfoURL = dto.MoreInfoURL; 125 | DownloadUrl = dto.DownloadUrl; 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/ExtensionManager.Manifest/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.Manifest.Internal; 2 | 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace ExtensionManager.Manifest; 6 | 7 | public static class ServiceCollectionExtensions 8 | { 9 | public static IServiceCollection AddManifestService(this IServiceCollection services) 10 | { 11 | services.AddTransient(); 12 | 13 | return services; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ExtensionManager.Shared/ExtensionManager.Shared.projitems: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | true 6 | e21ceac2-5e8c-4ae9-84ee-2e390cd272a9 7 | 8 | 9 | ExtensionManager 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/ExtensionManager.Shared/ExtensionManager.Shared.shproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | e21ceac2-5e8c-4ae9-84ee-2e390cd272a9 5 | 14.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/ExtensionManager.Shared/System.Runtime.CompilerServices/CallerArgumentExpressionAttribute.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace System.Runtime.CompilerServices; 5 | 6 | [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] 7 | internal sealed class CallerArgumentExpressionAttribute : Attribute 8 | { 9 | public CallerArgumentExpressionAttribute(string parameterName) 10 | { 11 | ParameterName = parameterName; 12 | } 13 | 14 | public string ParameterName { get; } 15 | } 16 | -------------------------------------------------------------------------------- /src/ExtensionManager.Shared/System.Runtime.CompilerServices/CompilerFeatureRequiredAttribute.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Diagnostics; 5 | using System.Diagnostics.CodeAnalysis; 6 | 7 | namespace System.Runtime.CompilerServices; 8 | 9 | /// 10 | /// Indicates that compiler support for a particular feature is required for the location where this attribute is applied. 11 | /// 12 | [ExcludeFromCodeCoverage, DebuggerNonUserCode] 13 | [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] 14 | internal sealed class CompilerFeatureRequiredAttribute : Attribute 15 | { 16 | public CompilerFeatureRequiredAttribute(string featureName) 17 | { 18 | FeatureName = featureName; 19 | } 20 | 21 | /// 22 | /// The name of the compiler feature. 23 | /// 24 | public string FeatureName { get; } 25 | 26 | /// 27 | /// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not understand . 28 | /// 29 | public bool IsOptional { get; init; } 30 | 31 | /// 32 | /// The used for the ref structs C# feature. 33 | /// 34 | public const string RefStructs = nameof(RefStructs); 35 | 36 | /// 37 | /// The used for the required members C# feature. 38 | /// 39 | public const string RequiredMembers = nameof(RequiredMembers); 40 | } 41 | -------------------------------------------------------------------------------- /src/ExtensionManager.Shared/System.Runtime.CompilerServices/RequiredMemberAttribute.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Diagnostics; 5 | using System.Diagnostics.CodeAnalysis; 6 | 7 | namespace System.Runtime.CompilerServices; 8 | 9 | /// 10 | /// Specifies that a type has required members or that a member is required. 11 | /// 12 | [ExcludeFromCodeCoverage, DebuggerNonUserCode] 13 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] 14 | internal sealed class RequiredMemberAttribute : Attribute 15 | { } 16 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/Attached/VSTheme.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | using ExtensionManager.VisualStudio.Themes; 4 | 5 | namespace ExtensionManager.UI.Attached; 6 | 7 | internal static class VSTheme 8 | { 9 | private static IVSThemes? _themes; 10 | 11 | public static readonly DependencyProperty UseProperty; 12 | 13 | static VSTheme() 14 | { 15 | UseProperty = DependencyProperty.RegisterAttached( 16 | "Use", 17 | typeof(bool), 18 | typeof(VSTheme), 19 | new FrameworkPropertyMetadata(false, OnUsePropertyChanged) 20 | { 21 | Inherits = true 22 | }); 23 | } 24 | 25 | public static void Initialize(IVSThemes themes) 26 | => Interlocked.Exchange(ref _themes, themes); 27 | 28 | public static void SetUse(UIElement element, bool value) => element.SetValue(UseProperty, value); 29 | public static bool GetUse(UIElement element) => (bool)element.GetValue(UseProperty); 30 | 31 | private static void OnUsePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 32 | { 33 | if (_themes is not { } themes) 34 | return; 35 | 36 | if (d is not UIElement element) 37 | return; 38 | if (e.NewValue is not bool value) 39 | return; 40 | 41 | themes.Use(element, value); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/Converters/AndConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Markup; 2 | 3 | namespace ExtensionManager.UI.Converters; 4 | 5 | [MarkupExtensionReturnType(typeof(AndConverter))] 6 | internal sealed class AndConverter : CombineBoolConverterBase 7 | { 8 | protected override bool Combine(bool value1, bool value2) => value1 && value2; 9 | } 10 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/Converters/CombineBoolConverterBase.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Windows.Data; 3 | using System.Windows.Markup; 4 | 5 | namespace ExtensionManager.UI.Converters; 6 | 7 | internal abstract class CombineBoolConverterBase : MarkupExtension, IMultiValueConverter 8 | { 9 | public bool Empty { get; set; } 10 | public bool CastFallback { get; set; } 11 | 12 | public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) 13 | { 14 | bool result; 15 | 16 | if (values.Length > 0) 17 | { 18 | result = true; 19 | 20 | for (var i = 0; i < values.Length; i++) 21 | { 22 | var value = values[i] as bool? ?? CastFallback; 23 | 24 | result = Combine(result, value); 25 | } 26 | } 27 | else 28 | result = Empty; 29 | 30 | return result; 31 | } 32 | 33 | public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) 34 | => throw new NotSupportedException(); 35 | 36 | public override object ProvideValue(IServiceProvider serviceProvider) => this; 37 | 38 | protected abstract bool Combine(bool value1, bool value2); 39 | } 40 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/Converters/InvertBoolConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Windows.Data; 3 | using System.Windows.Markup; 4 | 5 | namespace ExtensionManager.UI.Converters; 6 | 7 | [ValueConversion(typeof(bool), typeof(bool))] 8 | [MarkupExtensionReturnType(typeof(InvertBoolConverter))] 9 | internal sealed class InvertBoolConverter : MarkupExtension, IValueConverter 10 | { 11 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => Invert(value); 12 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => Invert(value); 13 | 14 | private object Invert(object value) 15 | { 16 | if (value is not bool boolValue) 17 | return value; 18 | 19 | boolValue = !boolValue; 20 | 21 | return boolValue; 22 | } 23 | 24 | public override object ProvideValue(IServiceProvider serviceProvider) => this; 25 | } 26 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/Converters/IsNullOrEmptyConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Windows.Data; 3 | using System.Windows.Markup; 4 | 5 | namespace ExtensionManager.UI.Converters; 6 | 7 | [ValueConversion(typeof(object), typeof(bool))] 8 | [MarkupExtensionReturnType(typeof(IsNullOrEmptyConverter))] 9 | internal sealed class IsNullOrEmptyConverter : MarkupExtension, IValueConverter 10 | { 11 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 12 | { 13 | if (value is null) 14 | return true; 15 | 16 | if (value is string s) 17 | return string.IsNullOrEmpty(s); 18 | 19 | return false; 20 | } 21 | 22 | public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 23 | => throw new NotSupportedException(); 24 | 25 | public override object ProvideValue(IServiceProvider serviceProvider) => this; 26 | } 27 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/Converters/IsTypeConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Windows.Data; 3 | using System.Windows.Markup; 4 | 5 | namespace ExtensionManager.UI.Converters; 6 | 7 | [ValueConversion(typeof(object), typeof(bool))] 8 | [MarkupExtensionReturnType(typeof(IsTypeConverter))] 9 | internal sealed class IsTypeConverter : MarkupExtension, IValueConverter 10 | { 11 | public Type? Type { get; set; } 12 | 13 | public object? Convert(object value, Type targetType, object? parameter, CultureInfo culture) 14 | { 15 | if (Type is null || value is null) 16 | return false; 17 | 18 | var valueType = value.GetType(); 19 | 20 | return Type.IsAssignableFrom(valueType); 21 | } 22 | 23 | public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 24 | => throw new NotSupportedException(); 25 | 26 | public override object ProvideValue(IServiceProvider serviceProvider) => this; 27 | } 28 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/DialogService.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.Manifest; 2 | using ExtensionManager.UI.ViewModels; 3 | using ExtensionManager.UI.Views; 4 | using ExtensionManager.UI.Worker; 5 | using ExtensionManager.VisualStudio.Extensions; 6 | using ExtensionManager.VisualStudio.Threads; 7 | 8 | using Microsoft.Win32; 9 | 10 | using WpfApplication = System.Windows.Application; 11 | 12 | namespace ExtensionManager.UI; 13 | 14 | internal sealed class DialogService : IDialogService 15 | { 16 | private readonly IVSThreads _threads; 17 | 18 | public DialogService(IVSThreads threads) 19 | { 20 | _threads = threads; 21 | } 22 | 23 | public Task ShowSaveVsextFileDialogAsync() => ShowVsextFileDialogAsync(); 24 | public Task ShowOpenVsextFileDialogAsync() => ShowVsextFileDialogAsync(); 25 | private Task ShowVsextFileDialogAsync() 26 | where TFileDialog : FileDialog, new() 27 | { 28 | if (_threads.CheckUIThreadAccess()) 29 | return Task.FromResult(OnUIThread()); 30 | 31 | return _threads.RunOnUIThreadAsync(OnUIThread); 32 | 33 | static string? OnUIThread() 34 | { 35 | TFileDialog dialog = new() 36 | { 37 | DefaultExt = ".vsext", 38 | FileName = "extensions", 39 | Filter = "VSEXT File|*.vsext" 40 | }; 41 | 42 | if (dialog.ShowDialog() == true) 43 | return dialog.FileName; 44 | 45 | return null; 46 | } 47 | } 48 | 49 | public Task ShowExportDialogAsync(IExportWorker worker, IManifest manifest, IReadOnlyCollection installedExtensions) 50 | => ShowExportDialogAsync(worker, manifest, installedExtensions, forSolution: false); 51 | public Task ShowExportForSolutionDialogAsync(IExportWorker worker, IManifest manifest, IReadOnlyCollection installedExtensions) 52 | => ShowExportDialogAsync(worker, manifest, installedExtensions, forSolution: true); 53 | private async Task ShowExportDialogAsync(IExportWorker worker, IManifest manifest, IReadOnlyCollection installedExtensions, bool forSolution) 54 | { 55 | var vm = new ExportDialogViewModel(worker, manifest, forSolution); 56 | 57 | foreach (var ext in installedExtensions) 58 | vm.Extensions.Add(new(ext)); 59 | 60 | await ShowInstallExportDialogAsync(vm); 61 | } 62 | 63 | public Task ShowInstallDialogAsync(IInstallWorker worker, IManifest manifest, IReadOnlyCollection extensions) 64 | => ShowInstallForSolutionDialogAsync(worker, manifest, extensions, forSolution: false); 65 | public Task ShowInstallForSolutionDialogAsync(IInstallWorker worker, IManifest manifest, IReadOnlyCollection extensions) 66 | => ShowInstallForSolutionDialogAsync(worker, manifest, extensions, forSolution: true); 67 | private async Task ShowInstallForSolutionDialogAsync(IInstallWorker worker, IManifest manifest, IReadOnlyCollection extensions, bool forSolution) 68 | { 69 | var vm = new InstallDialogViewModel(worker, manifest, forSolution); 70 | 71 | foreach (var (extension, status) in extensions) 72 | { 73 | switch (status) 74 | { 75 | case VSExtensionStatus.Installed: 76 | vm.AddExtension(extension, canBeSelected: false, group: "Already installed"); 77 | break; 78 | 79 | case VSExtensionStatus.NotInstalled: 80 | vm.AddExtension(extension, canBeSelected: true, group: "Extensions"); 81 | break; 82 | 83 | case VSExtensionStatus.NotSupported: 84 | vm.AddExtension(extension, canBeSelected: false, group: "Not supported"); 85 | break; 86 | 87 | default: 88 | throw new InvalidOperationException($"Unknown status {status} for extension {extension.Id}"); 89 | } 90 | } 91 | 92 | await ShowInstallExportDialogAsync(vm); 93 | } 94 | 95 | private Task ShowInstallExportDialogAsync(object viewModel) 96 | { 97 | if (_threads.CheckUIThreadAccess()) 98 | { 99 | OnUIThread(viewModel); 100 | 101 | return Task.CompletedTask; 102 | } 103 | 104 | return _threads.RunOnUIThreadAsync(() => OnUIThread(viewModel)); 105 | 106 | static void OnUIThread(object viewModel) 107 | { 108 | var window = new InstallExportDialogWindow 109 | { 110 | Owner = WpfApplication.Current.MainWindow, 111 | DataContext = viewModel 112 | }; 113 | 114 | try 115 | { 116 | window.ShowDialog(); 117 | } 118 | catch 119 | { 120 | window.Close(); 121 | 122 | throw; 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/ExtensionManager.UI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/IDialogService.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.Manifest; 2 | using ExtensionManager.UI.Worker; 3 | using ExtensionManager.VisualStudio.Extensions; 4 | 5 | namespace ExtensionManager.UI; 6 | 7 | public interface IDialogService 8 | { 9 | Task ShowSaveVsextFileDialogAsync(); 10 | Task ShowOpenVsextFileDialogAsync(); 11 | 12 | Task ShowExportDialogAsync(IExportWorker worker, IManifest manifest, IReadOnlyCollection installedExtensions); 13 | Task ShowExportForSolutionDialogAsync(IExportWorker worker, IManifest manifest, IReadOnlyCollection installedExtensions); 14 | Task ShowInstallDialogAsync(IInstallWorker worker, IManifest manifest, IReadOnlyCollection extensions); 15 | Task ShowInstallForSolutionDialogAsync(IInstallWorker worker, IManifest manifest, IReadOnlyCollection extensions); 16 | } 17 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace ExtensionManager.UI; 4 | 5 | public static class ServiceCollectionExtensions 6 | { 7 | public static IServiceCollection AddDialogService(this IServiceCollection services) 8 | { 9 | services.AddTransient(); 10 | 11 | return services; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/UIMarkupServices.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.UI.Attached; 2 | using ExtensionManager.VisualStudio.Themes; 3 | 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace ExtensionManager.UI; 7 | 8 | public static class UIMarkupServices 9 | { 10 | public static void Initialize(IServiceProvider services) 11 | { 12 | VSTheme.Initialize(services.GetRequiredService()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/Utils/ExtensionEqualityComparer.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.Extensions; 2 | 3 | namespace ExtensionManager.UI.Utils; 4 | 5 | internal sealed class ExtensionEqualityComparerById : IEqualityComparer 6 | { 7 | public static ExtensionEqualityComparerById Instance { get; } = new(); 8 | 9 | public bool Equals(IVSExtension x, IVSExtension y) 10 | => x?.Id == y?.Id; 11 | 12 | public int GetHashCode(IVSExtension obj) 13 | => obj?.Id?.GetHashCode() ?? 0; 14 | } 15 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/VSExtensionStatus.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.UI; 2 | 3 | public enum VSExtensionStatus 4 | { 5 | Unknown, 6 | Installed, 7 | NotInstalled, 8 | NotSupported, 9 | } 10 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/VSExtensionToInstall.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.Extensions; 2 | 3 | namespace ExtensionManager.UI; 4 | 5 | public record struct VSExtensionToInstall(IVSExtension Extension, VSExtensionStatus Status); 6 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/ViewModels/Base/DelegateCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Input; 2 | 3 | namespace ExtensionManager.UI.ViewModels.Base; 4 | 5 | internal sealed class DelegateCommand : ICommand 6 | { 7 | private readonly Func? _canExecute; 8 | private readonly Action _execute; 9 | 10 | public event EventHandler CanExecuteChanged 11 | { 12 | add => CommandManager.RequerySuggested += value; 13 | remove => CommandManager.RequerySuggested -= value; 14 | } 15 | 16 | public DelegateCommand(Action execute) 17 | { 18 | _execute = execute; 19 | } 20 | public DelegateCommand(Action execute, Func canExecute) 21 | { 22 | _canExecute = canExecute; 23 | _execute = execute; 24 | } 25 | 26 | public bool CanExecute(object parameter) 27 | => _canExecute is null || _canExecute(); 28 | 29 | public void Execute(object parameter) 30 | => _execute(); 31 | } 32 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/ViewModels/Base/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace ExtensionManager.UI.ViewModels.Base; 6 | 7 | internal abstract class ViewModelBase : INotifyPropertyChanged 8 | { 9 | public event PropertyChangedEventHandler? PropertyChanged; 10 | 11 | protected bool SetValue([NotNullIfNotNull(nameof(value))] ref T field, T value, [CallerMemberName] string propertyName = null!) 12 | { 13 | return SetValue(ref field, value, onChanged: null, propertyName); 14 | } 15 | protected bool SetValue([NotNullIfNotNull(nameof(value))] ref T field, T value, Action? onChanged, [CallerMemberName] string propertyName = null!) 16 | { 17 | if (!EqualityComparer.Default.Equals(field, value)) 18 | { 19 | field = value; 20 | 21 | NotifyPropertyChanged(propertyName); 22 | 23 | onChanged?.Invoke(); 24 | 25 | return true; 26 | } 27 | 28 | return false; 29 | } 30 | 31 | protected void NotifyPropertyChanged(string propertyName) 32 | { 33 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/ViewModels/ExportDialogViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | using ExtensionManager.Manifest; 4 | using ExtensionManager.UI.Worker; 5 | 6 | namespace ExtensionManager.UI.ViewModels; 7 | 8 | internal class ExportDialogViewModel : InstallExportDialogViewModel 9 | { 10 | private readonly IExportWorker _worker; 11 | private readonly IManifest _manifest; 12 | 13 | public ExportDialogViewModel(IExportWorker worker, IManifest manifest, bool forSolution) 14 | : base(forSolution ? InstallExportDialogType.ExportSolution : InstallExportDialogType.Export) 15 | { 16 | _worker = worker; 17 | _manifest = manifest; 18 | } 19 | 20 | protected override async Task DoWorkAsync(IProgress> progress, CancellationToken cancellationToken) 21 | { 22 | _manifest.Extensions.Clear(); 23 | 24 | foreach (var ext in SelectedExtensions) 25 | _manifest.Extensions.Add(ext.Model); 26 | 27 | await _worker.ExportAsync(_manifest, progress, cancellationToken); 28 | } 29 | 30 | protected override string? GetStepMessage(ExportStep step) 31 | { 32 | return step switch 33 | { 34 | ExportStep.None => null, 35 | ExportStep.SaveManifest => "Save manifest", 36 | ExportStep.Finish => "Finish export", 37 | _ => ReturnWithDebuggerBreak(), 38 | }; 39 | 40 | static string? ReturnWithDebuggerBreak() 41 | { 42 | Debugger.Break(); 43 | 44 | return null; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/ViewModels/ExtensionViewModel.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.UI.ViewModels.Base; 2 | using ExtensionManager.VisualStudio.Extensions; 3 | 4 | namespace ExtensionManager.UI.ViewModels; 5 | 6 | internal class ExtensionViewModel : ViewModelBase 7 | { 8 | private bool _canBeSelected = true; 9 | private bool _isSelected; 10 | private string? _group; 11 | 12 | public IVSExtension Model { get; } 13 | public string VsixID => Model.Id; 14 | public string? Name => Model.Name; 15 | public string? MoreInfoURL => Model.MoreInfoURL; 16 | 17 | public bool CanBeSelected 18 | { 19 | get => _canBeSelected; 20 | set 21 | { 22 | if (SetValue(ref _canBeSelected, value)) 23 | NotifyPropertyChanged(nameof(IsSelected)); 24 | } 25 | } 26 | public bool IsSelected 27 | { 28 | get => CanBeSelected && _isSelected; 29 | set => SetValue(ref _isSelected, value); 30 | } 31 | public string? Group 32 | { 33 | get => _group; 34 | set => SetValue(ref _group, value); 35 | } 36 | 37 | public ExtensionViewModel(IVSExtension model) 38 | { 39 | Model = model; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/ViewModels/InstallDialogViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | using ExtensionManager.Manifest; 4 | using ExtensionManager.UI.Worker; 5 | using ExtensionManager.VisualStudio.Extensions; 6 | 7 | namespace ExtensionManager.UI.ViewModels; 8 | 9 | internal class InstallDialogViewModel : InstallExportDialogViewModel 10 | { 11 | private readonly IInstallWorker _worker; 12 | private readonly IManifest _manifest; 13 | 14 | public InstallDialogViewModel(IInstallWorker worker, IManifest manifest, bool forSolution) 15 | : base(forSolution ? InstallExportDialogType.InstallSolution : InstallExportDialogType.Install) 16 | { 17 | _worker = worker; 18 | _manifest = manifest; 19 | } 20 | 21 | public void AddExtension(IVSExtension extension, bool canBeSelected, string? group) 22 | { 23 | Extensions.Add(new(extension) 24 | { 25 | CanBeSelected = canBeSelected, 26 | Group = group, 27 | }); 28 | } 29 | 30 | protected override async Task DoWorkAsync(IProgress> progress, CancellationToken cancellationToken) 31 | { 32 | var extensions = SelectedExtensions.Select(x => x.Model).ToArray(); 33 | 34 | await _worker.InstallAsync(_manifest, extensions, SystemWide, progress, cancellationToken); 35 | } 36 | 37 | protected override string? GetStepMessage(InstallStep step) 38 | { 39 | return step switch 40 | { 41 | InstallStep.None => null, 42 | InstallStep.DownloadData => "Download extension data", 43 | InstallStep.DownloadVsix => "Download extension files", 44 | InstallStep.RunInstallation => "Start installation", 45 | _ => ReturnWithDebuggerBreak(), 46 | }; 47 | 48 | static string? ReturnWithDebuggerBreak() 49 | { 50 | Debugger.Break(); 51 | 52 | return null; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/ViewModels/InstallExportDialogType.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.UI.ViewModels; 2 | 3 | internal enum InstallExportDialogType 4 | { 5 | Export, 6 | ExportSolution, 7 | Install, 8 | InstallSolution 9 | } 10 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/ViewModels/InstallExportDialogViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.Collections.Specialized; 3 | using System.ComponentModel; 4 | using System.Windows.Input; 5 | 6 | using ExtensionManager.UI.ViewModels.Base; 7 | using ExtensionManager.UI.Worker; 8 | 9 | namespace ExtensionManager.UI.ViewModels; 10 | 11 | internal abstract class InstallExportDialogViewModel : InstallExportDialogViewModel, IProgress> 12 | { 13 | private CancellationTokenSource? _cts; 14 | 15 | protected InstallExportDialogViewModel(InstallExportDialogType dialogType) 16 | : base(dialogType) 17 | { 18 | } 19 | 20 | protected override bool CanOk() 21 | { 22 | return base.CanOk() 23 | && !IsRunning; 24 | } 25 | protected override async void OnOk() 26 | { 27 | await RunWorkAsync(); 28 | 29 | RequestClose(); 30 | } 31 | private async Task RunWorkAsync() 32 | { 33 | using var cts = new CancellationTokenSource(); 34 | 35 | Interlocked.Exchange(ref _cts, cts); 36 | 37 | IsRunning = true; 38 | 39 | try 40 | { 41 | await DoWorkAsync(this, cts.Token); 42 | } 43 | catch (OperationCanceledException) 44 | when (cts.IsCancellationRequested) 45 | { 46 | } 47 | finally 48 | { 49 | IsRunning = false; 50 | 51 | Interlocked.CompareExchange(ref _cts, null, cts); 52 | } 53 | } 54 | 55 | protected override void OnCancel() 56 | { 57 | _cts?.Cancel(); 58 | 59 | RequestClose(); 60 | } 61 | 62 | public override void OnClosed() 63 | { 64 | var cts = Interlocked.Exchange(ref _cts, null); 65 | 66 | if (cts is not null) 67 | { 68 | cts.Cancel(); 69 | cts.Dispose(); 70 | } 71 | } 72 | 73 | protected abstract Task DoWorkAsync(IProgress> progress, CancellationToken cancellationToken); 74 | protected abstract string? GetStepMessage(TStep step); 75 | 76 | void IProgress>.Report(ProgressStep value) 77 | { 78 | ProgressText = GetStepMessage(value.Step); 79 | ProgressPercentage = value.Percentage; 80 | } 81 | } 82 | 83 | internal abstract class InstallExportDialogViewModel : ViewModelBase 84 | { 85 | private bool _systemWide; 86 | private bool _isRunning; 87 | private string? _progressText; 88 | private float? _progressPercentage; 89 | 90 | public event EventHandler? CloseRequested; 91 | 92 | public ObservableCollection Extensions { get; } = new(); 93 | 94 | public bool HasAllSelected 95 | { 96 | get 97 | { 98 | var hasAny = false; 99 | 100 | foreach (var ext in Extensions) 101 | { 102 | if (!ext.CanBeSelected) 103 | continue; 104 | 105 | if (!ext.IsSelected) 106 | return false; 107 | 108 | hasAny = true; 109 | } 110 | 111 | return hasAny; 112 | } 113 | set 114 | { 115 | foreach (var item in Extensions) 116 | { 117 | if (item.CanBeSelected) 118 | item.IsSelected = value; 119 | } 120 | } 121 | } 122 | 123 | public bool HasAnySelected => Extensions.Any(x => x.IsSelected); 124 | public IEnumerable SelectedExtensions => Extensions.Where(x => x.IsSelected); 125 | 126 | public bool SystemWide 127 | { 128 | get => _systemWide; 129 | set => SetValue(ref _systemWide, value); 130 | } 131 | 132 | public bool IsRunning 133 | { 134 | get => _isRunning; 135 | protected set => SetValue(ref _isRunning, value); 136 | } 137 | public string? ProgressText 138 | { 139 | get => _progressText; 140 | protected set => SetValue(ref _progressText, value); 141 | } 142 | public float? ProgressPercentage 143 | { 144 | get => _progressPercentage; 145 | protected set => SetValue(ref _progressPercentage, value); 146 | } 147 | 148 | public InstallExportDialogType DialogType { get; } 149 | 150 | public ICommand OkCommand { get; } 151 | public ICommand CancelCommand { get; } 152 | 153 | protected InstallExportDialogViewModel(InstallExportDialogType dialogType) 154 | { 155 | DialogType = dialogType; 156 | 157 | OkCommand = new DelegateCommand(OnOk, CanOk); 158 | CancelCommand = new DelegateCommand(OnCancel, CanCancel); 159 | 160 | Extensions.CollectionChanged += OnExtensionsCollectionChanged; 161 | } 162 | 163 | protected virtual bool CanOk() => HasAnySelected; 164 | protected abstract void OnOk(); 165 | 166 | protected virtual bool CanCancel() => true; 167 | protected abstract void OnCancel(); 168 | 169 | public virtual void OnClosed() { } 170 | 171 | protected void RequestClose() 172 | => CloseRequested?.Invoke(this, EventArgs.Empty); 173 | 174 | private void OnExtensionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 175 | { 176 | if (e.OldItems is not null) 177 | { 178 | foreach (var item in e.OldItems.OfType()) 179 | item.PropertyChanged -= OnExtensionPropertyChanged; 180 | } 181 | 182 | if (e.NewItems is not null) 183 | { 184 | foreach (var item in e.NewItems.OfType()) 185 | item.PropertyChanged += OnExtensionPropertyChanged; 186 | } 187 | } 188 | 189 | private void OnExtensionPropertyChanged(object sender, PropertyChangedEventArgs e) 190 | { 191 | switch (e.PropertyName) 192 | { 193 | case nameof(ExtensionViewModel.IsSelected): 194 | NotifyPropertyChanged(nameof(HasAllSelected)); 195 | NotifyPropertyChanged(nameof(HasAnySelected)); 196 | NotifyPropertyChanged(nameof(SelectedExtensions)); 197 | break; 198 | 199 | case nameof(ExtensionViewModel.CanBeSelected): 200 | NotifyPropertyChanged(nameof(HasAllSelected)); 201 | break; 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/Views/InstallExportDialogWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Windows; 3 | using System.Windows.Input; 4 | using System.Windows.Navigation; 5 | 6 | using ExtensionManager.UI.ViewModels; 7 | using ExtensionManager.UI.Win32; 8 | 9 | namespace ExtensionManager.UI.Views; 10 | 11 | internal partial class InstallExportDialogWindow : Window 12 | { 13 | public InstallExportDialogWindow() 14 | { 15 | WindowStartupLocation = WindowStartupLocation.CenterOwner; 16 | 17 | DataContextChanged += OnDataContextChanged; 18 | 19 | InitializeComponent(); 20 | } 21 | 22 | private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) 23 | { 24 | if (e.NewValue is InstallExportDialogViewModel vm) 25 | { 26 | vm.CloseRequested -= OnCloseRequested; 27 | vm.CloseRequested += OnCloseRequested; 28 | } 29 | } 30 | 31 | private void OnCloseRequested(object sender, EventArgs e) 32 | { 33 | if (sender is not InstallExportDialogViewModel vm) 34 | return; 35 | 36 | if (!ReferenceEquals(vm, DataContext)) 37 | { 38 | vm.CloseRequested -= OnCloseRequested; 39 | return; 40 | } 41 | 42 | Close(); 43 | } 44 | 45 | protected override void OnSourceInitialized(EventArgs e) 46 | { 47 | base.OnSourceInitialized(e); 48 | 49 | this.StyleWindowAsDialogBox(); 50 | } 51 | 52 | protected override void OnMouseDown(MouseButtonEventArgs e) 53 | { 54 | base.OnMouseDown(e); 55 | 56 | if (e.ChangedButton == MouseButton.Left) 57 | DragMove(); 58 | } 59 | 60 | protected override void OnClosed(EventArgs e) 61 | { 62 | base.OnClosed(e); 63 | 64 | if (DataContext is InstallExportDialogViewModel vm) 65 | vm.OnClosed(); 66 | } 67 | 68 | private void OnHyperlinkRequestNavigate(object sender, RequestNavigateEventArgs e) 69 | { 70 | Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri) 71 | { 72 | UseShellExecute = true, 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/Win32/NativeMethods.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Interop; 3 | 4 | namespace ExtensionManager.UI.Win32; 5 | 6 | internal static class NativeMethods 7 | { 8 | /// 9 | /// Associates a new large or small icon with a window. The system displays the large icon in the ALT+TAB dialog box, and the small icon in the window caption. 10 | /// 11 | private const int WM_SETICON = 0x0080; 12 | 13 | // from winuser.h 14 | private const int GWL_STYLE = -16; 15 | private const int WS_DLGFRAME = 0x00400000; 16 | private const int WS_MAXIMIZEBOX = 0x10000; 17 | private const int WS_MINIMIZEBOX = 0x20000; 18 | 19 | private const int GWL_EXSTYLE = -20; 20 | private const int WS_EX_DLGMODALFRAME = 0x0001; 21 | private const int SWP_NOSIZE = 0x0001; 22 | private const int SWP_NOMOVE = 0x0002; 23 | private const int SWP_NOZORDER = 0x0004; 24 | private const int SWP_FRAMECHANGED = 0x0020; 25 | 26 | /// 27 | /// Changes the border of the window to the style commonly utilized for dialog boxes. 28 | /// 29 | /// 30 | /// thanks stack overflow 31 | /// 32 | public static void StyleWindowAsDialogBox(this Window window) 33 | { 34 | if (window is null) 35 | return; 36 | 37 | var hwnd = new WindowInteropHelper(window).Handle; 38 | 39 | if (!User32.IsWindow(hwnd)) 40 | return; 41 | 42 | SetDialogWindowFrame(hwnd); 43 | HideMaximizeButton(hwnd); 44 | HideMinimizeButton(hwnd); 45 | RemoveIcon(hwnd); 46 | } 47 | 48 | /// 49 | /// Removes the Maximize button from a 's title bar. 50 | /// 51 | /// 52 | /// thanks stack overflow 53 | /// 54 | private static void HideMaximizeButton(IntPtr hwnd) 55 | { 56 | var currentStyle = User32.GetWindowLong(hwnd, GWL_STYLE); 57 | 58 | User32.SetWindowLong(hwnd, GWL_STYLE, currentStyle & ~WS_MAXIMIZEBOX); 59 | } 60 | 61 | /// 62 | /// Removes the Maximize button from a 's title bar. 63 | /// 64 | /// 65 | /// thanks stack overflow 66 | /// 67 | private static void HideMinimizeButton(IntPtr hwnd) 68 | { 69 | var currentStyle = User32.GetWindowLong(hwnd, GWL_STYLE); 70 | 71 | User32.SetWindowLong(hwnd, GWL_STYLE, currentStyle & ~WS_MINIMIZEBOX); 72 | } 73 | 74 | /// 75 | /// Removes the icon from the title bar of the referred to by the parameter. 76 | /// 77 | /// Reference to an instance of a from which the icon is to be removed. 78 | /// Thank you Stack Overflow. 79 | private static void RemoveIcon(IntPtr hwnd) 80 | { 81 | // Change the extended window style to not show a window icon 82 | var extendedStyle = User32.GetWindowLong(hwnd, GWL_EXSTYLE); 83 | 84 | User32.SetWindowLong(hwnd, GWL_EXSTYLE, extendedStyle | WS_EX_DLGMODALFRAME); 85 | 86 | User32.SendMessage(hwnd, WM_SETICON, 1, IntPtr.Zero); 87 | User32.SendMessage(hwnd, WM_SETICON, 0, IntPtr.Zero); 88 | 89 | // Update the window's non-client area to reflect the changes 90 | User32.SetWindowPos(hwnd, IntPtr.Zero, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); 91 | } 92 | 93 | /// 94 | /// Changes the border of the window to the style commonly utilized for dialog boxes. 95 | /// 96 | /// 97 | /// thanks stack overflow 98 | /// 99 | private static void SetDialogWindowFrame(IntPtr hwnd) 100 | { 101 | var currentStyle = User32.GetWindowLong(hwnd, GWL_STYLE); 102 | 103 | User32.SetWindowLong(hwnd, GWL_STYLE, currentStyle | WS_DLGFRAME); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/Win32/User32.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ExtensionManager.UI.Win32; 4 | 5 | internal class User32 6 | { 7 | [DllImport("user32.dll")] 8 | public static extern bool SetWindowPos(IntPtr hwnd, IntPtr hwndInsertAfter, int x, int y, int width, int height, uint flags); 9 | 10 | [DllImport("user32.dll")] 11 | public static extern IntPtr SendMessage(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam); 12 | 13 | [DllImport("user32.dll")] 14 | public extern static int GetWindowLong(IntPtr hwnd, int index); 15 | 16 | [DllImport("user32.dll")] 17 | public extern static int SetWindowLong(IntPtr hwnd, int index, int value); 18 | 19 | /// 20 | /// Determines whether the specified window handle identifies an existing window. 21 | /// 22 | /// A handle to the window to be tested. 23 | /// If the window handle identifies an existing window, the return value is nonzero. If the window handle does not identify an existing window, the return value is zero. 24 | /// A thread should not use IsWindow for a window that it did not create because the window could be destroyed after this function was called. Further, because window handles are recycled the handle could even point to a different window. 25 | [DllImport("user32.dll")] 26 | public static extern bool IsWindow(IntPtr hWnd); 27 | 28 | /// 29 | /// Sends the specified message to a window or windows. The SendMessage function calls the window procedure for the specified window and does not return until the window procedure has processed the message.To send a message and return immediately, use the SendMessageCallback or SendNotifyMessage function. To post a message to a thread's message queue and return immediately, use the PostMessage or PostThreadMessage function. 30 | /// 31 | /// A handle to the window whose window procedure will receive the message. If this parameter is HWND_BROADCAST ((HWND)0xffff), the message is sent to all top-level windows in the system, including disabled or invisible unowned windows, overlapped windows, and pop-up windows; but the message is not sent to child windows.Message sending is subject to UIPI. The thread of a process can send messages only to message queues of threads in processes of lesser or equal integrity level. 32 | /// The message to be sent.For lists of the system-provided messages, see System-Defined Messages. 33 | /// Additional message-specific information. 34 | /// Additional message-specific information. 35 | /// 36 | /// The return value specifies the result of the message processing; it depends on the message sent. 37 | /// When a message is blocked by UIPI the last error, retrieved with GetLastError, is set to 5 (access denied).Applications that need to communicate using HWND_BROADCAST should use the RegisterWindowMessage function to obtain a unique message for inter-application communication.The system only does marshalling for system messages (those in the range 0 to (WM_USER-1)). To send other messages (those >= WM_USER) to another process, you must do custom marshalling.If the specified window was created by the calling thread, the window procedure is called immediately as a subroutine. If the specified window was created by a different thread, the system switches to that thread and calls the appropriate window procedure. Messages sent between threads are processed only when the receiving thread executes message retrieval code. The sending thread is blocked until the receiving thread processes the message. However, the sending thread will process incoming nonqueued messages while waiting for its message to be processed. To prevent this, use SendMessageTimeout with SMTO_BLOCK set. For more information on nonqueued messages, see Nonqueued Messages.An accessibility application can use SendMessage to send WM_APPCOMMAND messages to the shell to launch applications. This functionality is not guaranteed to work for other types of applications. 38 | [DllImport("user32.dll", CharSet = CharSet.Auto)] 39 | public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, IntPtr lParam); 40 | } 41 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/Worker/ExportStep.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.UI.Worker; 2 | 3 | public enum ExportStep 4 | { 5 | None = 0, 6 | SaveManifest, 7 | Finish, 8 | } 9 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/Worker/IExportWorker.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.Manifest; 2 | 3 | namespace ExtensionManager.UI.Worker; 4 | 5 | public interface IExportWorker 6 | { 7 | Task ExportAsync(IManifest manifest, IProgress> progress, CancellationToken cancellationToken); 8 | } 9 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/Worker/IInstallWorker.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.Manifest; 2 | using ExtensionManager.VisualStudio.Extensions; 3 | 4 | namespace ExtensionManager.UI.Worker; 5 | 6 | public interface IInstallWorker 7 | { 8 | Task InstallAsync(IManifest manifest, IReadOnlyCollection extensions, bool systemWide, IProgress> progress, CancellationToken cancellationToken); 9 | } 10 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/Worker/InstallStep.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.UI.Worker; 2 | 3 | public enum InstallStep 4 | { 5 | None = 0, 6 | DownloadData, 7 | DownloadVsix, 8 | RunInstallation, 9 | } 10 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/Worker/ProgressStep.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.UI.Worker; 2 | 3 | public record struct ProgressStep(float? Percentage, TStep Step); 4 | -------------------------------------------------------------------------------- /src/ExtensionManager.UI/Worker/ProgressStepExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.UI.Worker; 2 | 3 | public static class ProgressStepExtensions 4 | { 5 | public static void Report(this IProgress> progress, float? percentage, TStep step) 6 | => progress.Report(new(percentage, step)); 7 | } 8 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Abstractions/Documents/IVSDocuments.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.VisualStudio.Documents; 2 | 3 | /// 4 | /// Contains helper methods for dealing with documents. 5 | /// 6 | public interface IVSDocuments 7 | { 8 | /// 9 | /// Opens a file in editor window. 10 | /// 11 | Task OpenAsync(string file); 12 | } 13 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Abstractions/ExtensionManager.VisualStudio.Abstractions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ExtensionManager.VisualStudio 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Abstractions/Extensions/IVSExtension.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.VisualStudio.Extensions; 2 | 3 | /// 4 | /// Represents a Visual Studio extension. 5 | /// 6 | public interface IVSExtension 7 | { 8 | /// 9 | /// Gets the unique identifier for the extension. 10 | /// 11 | string Id { get; } 12 | 13 | /// 14 | /// Gets the name of the extension. 15 | /// 16 | string? Name { get; } 17 | 18 | /// 19 | /// Gets the URL for more information about the extension. 20 | /// 21 | string? MoreInfoURL { get; } 22 | 23 | /// 24 | /// Gets the URL to download the extension. 25 | /// 26 | string? DownloadUrl { get; } 27 | } 28 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Abstractions/Extensions/IVSExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.VisualStudio.Extensions; 2 | 3 | /// 4 | /// Contains methods for dealing with Visual Studio Extensions. 5 | /// 6 | public interface IVSExtensions 7 | { 8 | /// 9 | /// Retrieves the IDs of all installed extensions. 10 | /// 11 | Task> GetInstalledExtensionsAsync(); 12 | 13 | /// 14 | /// Downloads extension data based on the specified extension IDs 15 | /// 16 | Task> GetGalleryExtensionsAsync(IEnumerable extensionIds); 17 | 18 | /// 19 | /// Starts the VsixInstaller with the specified vsix files. 20 | /// 21 | Task StartInstallerAsync(IEnumerable vsixFiles, bool systemWide); 22 | } 23 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Abstractions/IVSServicesRegistrar.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace ExtensionManager.VisualStudio; 4 | 5 | /// 6 | /// Defines a mechanism for registering services for interacting with Visual Studio. 7 | /// 8 | public interface IVSServicesRegistrar 9 | { 10 | /// 11 | /// Registers services for Visual Studio interaction into the provided service collection. 12 | /// 13 | void AddServices(IServiceCollection services); 14 | } 15 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Abstractions/MessageBox/IVSMessageBox.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.VisualStudio.MessageBox; 2 | 3 | /// 4 | /// Shows message boxes. 5 | /// 6 | public interface IVSMessageBox 7 | { 8 | /// 9 | /// Shows an error message box. 10 | /// 11 | Task ShowErrorAsync(string line1, string line2 = ""); 12 | 13 | /// 14 | /// Shows a warning message box. 15 | /// 16 | /// if the OK button was clicked, otherwise. 17 | Task ShowWarningAsync(string line1, string line2 = ""); 18 | } 19 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Abstractions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace ExtensionManager.VisualStudio; 4 | 5 | /// 6 | /// Extensions for the to facilitate the configuration of services related to Visual Studio. 7 | /// 8 | public static class ServiceCollectionExtensions 9 | { 10 | /// 11 | /// Configures services for interacting with Visual Studio by utilizing a specified . 12 | /// 13 | public static IServiceCollection ConfigureVSServices(this IServiceCollection services, IVSServicesRegistrar registrar) 14 | { 15 | registrar.AddServices(services); 16 | return services; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Abstractions/Solution/IVSSolution.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.VisualStudio.Solution; 2 | 3 | /// 4 | /// Represents the solution itself. 5 | /// 6 | public interface IVSSolution : IVSSolutionItem 7 | { 8 | /// 9 | /// Adds a solution folder to the solution 10 | /// 11 | /// 12 | /// 13 | Task AddSolutionFolderAsync(string name); 14 | } 15 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Abstractions/Solution/IVSSolutionFolder.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.VisualStudio.Solution; 2 | 3 | /// 4 | /// Represents a solution folder in the solution 5 | /// 6 | public interface IVSSolutionFolder : IVSSolutionItem 7 | { 8 | /// 9 | /// Adds one or more files to the solution folder. 10 | /// 11 | Task AddExistingFilesAsync(params string[] files); 12 | } 13 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Abstractions/Solution/IVSSolutionItem.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace ExtensionManager.VisualStudio.Solution; 3 | 4 | /// 5 | /// Represents a file, folder, project, or other item in Solution Explorer. 6 | /// 7 | public interface IVSSolutionItem 8 | { 9 | /// 10 | /// The name of the item. 11 | /// 12 | string Name { get; } 13 | 14 | /// 15 | /// The absolute file path on disk. 16 | /// 17 | string? FullPath { get; } 18 | 19 | /// 20 | /// Gets the children of this solution item. 21 | /// 22 | Task> GetChildrenAsync(); 23 | } 24 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Abstractions/Solution/IVSSolutions.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.VisualStudio.Solution; 2 | 3 | /// 4 | /// A collection of services related to solutions. 5 | /// 6 | public interface IVSSolutions 7 | { 8 | /// 9 | /// Checks if a solution is open. 10 | /// 11 | Task IsOpenAsync(); 12 | 13 | /// 14 | /// Gets the current solution. 15 | /// 16 | Task GetCurrentSolutionAsync(); 17 | } 18 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Abstractions/StatusBar/IVSStatusBar.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.VisualStudio.StatusBar; 2 | 3 | /// 4 | /// An API wrapper that makes it easy to work with the status bar. 5 | /// 6 | public interface IVSStatusBar 7 | { 8 | /// 9 | /// Clears all text from the status bar. 10 | /// 11 | Task ClearAsync(); 12 | 13 | /// 14 | /// Sets the text in the status bar. 15 | /// 16 | Task ShowMessageAsync(string text); 17 | 18 | /// 19 | /// Shows the progress indicator in the status bar. 20 | /// Set and 21 | /// to the same value to stop the progress. 22 | /// 23 | /// The text to display in the status bar. 24 | /// The current step number starting at 1. 25 | /// The total number of steps to completion. 26 | Task ShowProgressAsync(string text, int currentStep, int numberOfSteps); 27 | } 28 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Abstractions/Themes/IVSThemes.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace ExtensionManager.VisualStudio.Themes; 4 | 5 | /// 6 | /// Contains methods for WPF to deal with Visual Studio Themes. 7 | /// 8 | public interface IVSThemes 9 | { 10 | /// 11 | /// Sets a value that enables or disables whether each XAML control or window should be styled automatically using the VS theme properties. 12 | /// 13 | void Use(UIElement element, bool use = true); 14 | } 15 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Abstractions/Threads/IVSThreads.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.VisualStudio.Threads; 2 | 3 | /// 4 | /// Contains methods for dealing with threads. 5 | /// 6 | public interface IVSThreads 7 | { 8 | /// 9 | /// Determines if the call is being made on the UI thread. 10 | /// 11 | /// if the call is on the UI thread. 12 | bool CheckUIThreadAccess(); 13 | 14 | /// 15 | /// Executes the method on the UI thread. 16 | /// 17 | Task RunOnUIThreadAsync(Action syncMethod); 18 | 19 | /// 20 | /// Executes the method on the UI thread. 21 | /// 22 | Task RunOnUIThreadAsync(Func syncMethod); 23 | } 24 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Abstractions/ExtensionManager.VisualStudio.Adapter.Abstractions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ExtensionManager.VisualStudio.Adapter 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Abstractions/Extensions/IVSExtensionManagerAdapter.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.VisualStudio.Adapter.Extensions; 2 | 3 | public interface IVSExtensionManagerAdapter 4 | { 5 | Task> GetInstalledExtensionsAsync(); 6 | } 7 | 8 | public interface IVSExtensionManagerAdapter 9 | { 10 | Task GetManagerAsync(); 11 | IEnumerable GetInstalledExtensions(TManager manager); 12 | IVSInstalledExtensionInfo CreateInstalledExtensionInfo(TInstalledExtension extension); 13 | } 14 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Abstractions/Extensions/IVSExtensionRepositoryAdapter.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.Extensions; 2 | 3 | namespace ExtensionManager.VisualStudio.Adapter.Extensions; 4 | 5 | public interface IVSExtensionRepositoryAdapter 6 | { 7 | Task> GetVSGalleryExtensionsAsync(List extensionIds, int lcid, bool forAutoupdate); 8 | } 9 | 10 | 11 | public interface IVSExtensionRepositoryAdapter 12 | { 13 | Task GetRepositoryAsync(); 14 | IEnumerable GetVSGalleryExtensions(TRepository repository, List extensionIds, int lcid, bool forAutoupdate); 15 | } 16 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Abstractions/Extensions/IVSInstalledExtensionInfo.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.VisualStudio.Adapter.Extensions; 2 | 3 | public interface IVSInstalledExtensionInfo 4 | { 5 | string Identifier { get; } 6 | bool IsSystemComponent { get; } 7 | bool IsPackComponent { get; } 8 | } 9 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Abstractions/Extensions/VSExtensionManagerAdapter.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.VisualStudio.Adapter.Extensions; 2 | 3 | public sealed class VSExtensionManagerAdapter : IVSExtensionManagerAdapter 4 | where TManager : class 5 | where TInstalledExtension : class 6 | { 7 | private readonly IVSExtensionManagerAdapter _genericAdapter; 8 | 9 | public VSExtensionManagerAdapter(IVSExtensionManagerAdapter genericAdapter) 10 | => _genericAdapter = genericAdapter; 11 | 12 | public async Task> GetInstalledExtensionsAsync() 13 | { 14 | var manager = await _genericAdapter.GetManagerAsync(); 15 | 16 | return _genericAdapter.GetInstalledExtensions(manager) 17 | .Select(_genericAdapter.CreateInstalledExtensionInfo) 18 | .ToList(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Abstractions/Extensions/VSExtensionRepositoryAdapter.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.Extensions; 2 | 3 | namespace ExtensionManager.VisualStudio.Adapter.Extensions; 4 | 5 | public sealed class VSExtensionRepositoryAdapter : IVSExtensionRepositoryAdapter 6 | where TRepository : class 7 | { 8 | private readonly IVSExtensionRepositoryAdapter _genericAdapter; 9 | 10 | public VSExtensionRepositoryAdapter(IVSExtensionRepositoryAdapter genericAdapter) 11 | => _genericAdapter = genericAdapter; 12 | 13 | public async Task> GetVSGalleryExtensionsAsync(List extensionIds, int lcid, bool forAutoupdate) 14 | { 15 | var repository = await _genericAdapter.GetRepositoryAsync(); 16 | 17 | return _genericAdapter 18 | .GetVSGalleryExtensions(repository, extensionIds, lcid, forAutoupdate) 19 | .ToList(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Abstractions/IVSAdapterServicesFactory.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.Adapter.Extensions; 2 | 3 | namespace ExtensionManager.VisualStudio.Adapter; 4 | 5 | public interface IVSAdapterServicesFactory 6 | { 7 | IVSExtensionManagerAdapter CreateExtensionManagerAdapter(); 8 | IVSExtensionRepositoryAdapter CreateExtensionRepositoryAdapter(); 9 | } 10 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Generator/ExtensionManager.VisualStudio.Adapter.Generator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Generator/Internal/Emitter/ClassEmitter.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.Adapter.Generator.Internal.Utils; 2 | 3 | namespace ExtensionManager.VisualStudio.Adapter.Generator.Internal.Emitter; 4 | 5 | internal sealed class ClassEmitter 6 | { 7 | private readonly TypeBuilder _builder; 8 | 9 | public ClassEmitter(TypeBuilder builder) 10 | => _builder = builder; 11 | 12 | public void Implement(Type type) 13 | => _builder.AddInterfaceImplementation(type); 14 | 15 | public void Implement(Action> emit) 16 | { 17 | _builder.AddInterfaceImplementation(typeof(TInterface)); 18 | emit(new InterfaceImplementationEmitter(this)); 19 | } 20 | 21 | public FieldBuilder Field(string name, Type type, FieldAttributes attributes) 22 | => _builder.DefineField(name, type, attributes); 23 | 24 | public PropertyBuilder Property(string name, Type type, Action emit) 25 | { 26 | var builder = _builder.DefineProperty(name, PropertyAttributes.None, type, null); 27 | emit(new PropertyEmitter(this, builder)); 28 | return builder; 29 | } 30 | 31 | public MethodBuilder ImplementMethod(MethodInfo methodInfo) 32 | { 33 | var methodAttributes = methodInfo.GetMethodAttributes(); 34 | var parameterTypes = methodInfo.GetParameters().SelectArray(p => p.ParameterType); 35 | 36 | return Method(methodInfo.Name, methodAttributes, methodInfo.ReturnType, parameterTypes); 37 | } 38 | 39 | public MethodBuilder Method(string name, MethodAttributes attributes, Type returnType, Type[] parameters) 40 | => _builder.DefineMethod(name, attributes, returnType, parameters); 41 | 42 | public void AutoCtor(FieldBuilder field) 43 | { 44 | Ctor(field.FieldType) 45 | .EmitIL(il => 46 | { 47 | il.EmitCallEmptyBaseCtor(_builder.BaseType); 48 | il.Emit(OpCodes.Ldarg_0); 49 | il.Emit(OpCodes.Ldarg_1); 50 | il.Emit(OpCodes.Stfld, field); 51 | il.Emit(OpCodes.Ret); 52 | }); 53 | } 54 | 55 | public ConstructorBuilder Ctor(params Type[] parameters) 56 | { 57 | const MethodAttributes ctorAttributes = MethodAttributes.Public 58 | | MethodAttributes.HideBySig 59 | | MethodAttributes.SpecialName 60 | | MethodAttributes.RTSpecialName; 61 | 62 | return _builder.DefineConstructor(ctorAttributes, CallingConventions.Standard, parameters); 63 | } 64 | 65 | public ConstructorBuilder DefaultCtor() 66 | { 67 | return Ctor(parameters: []) 68 | .EmitIL(il => 69 | { 70 | il.EmitCallEmptyBaseCtor(_builder.BaseType); 71 | il.Emit(OpCodes.Ret); 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Generator/Internal/Emitter/InterfaceImplementationEmitter.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace ExtensionManager.VisualStudio.Adapter.Generator.Internal.Emitter; 4 | 5 | internal sealed class InterfaceImplementationEmitter 6 | { 7 | private readonly ClassEmitter _emitter; 8 | 9 | public InterfaceImplementationEmitter(ClassEmitter emitter) 10 | => _emitter = emitter; 11 | 12 | public void Property(Expression> getPropertyExpression, Action emit) 13 | { 14 | var propertyInfo = GetPropertyInfo(getPropertyExpression); 15 | 16 | _emitter.Property(propertyInfo.Name, propertyInfo.PropertyType, emitter => emit(emitter, propertyInfo)); 17 | } 18 | 19 | public MethodBuilder Method(Expression> callMethodExpression) => EmiMethod(callMethodExpression); 20 | public MethodBuilder Method(Expression> callMethodExpression) => EmiMethod(callMethodExpression); 21 | private MethodBuilder EmiMethod(LambdaExpression callMethodExpression) 22 | => _emitter.ImplementMethod(GetMethodInfo(callMethodExpression)); 23 | 24 | private PropertyInfo GetPropertyInfo(LambdaExpression getPropertyExpression) 25 | { 26 | if (getPropertyExpression.Body is MemberExpression { Member: PropertyInfo propertyInfo }) 27 | return propertyInfo; 28 | 29 | throw new ArgumentException("Expression is not a valid property expression.", nameof(getPropertyExpression)); 30 | } 31 | 32 | private MethodInfo GetMethodInfo(LambdaExpression callMethodExpression) 33 | { 34 | if (callMethodExpression.Body is MethodCallExpression { Method: var method }) 35 | return method; 36 | 37 | throw new ArgumentException("Expression is not a valid method expression.", nameof(callMethodExpression)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Generator/Internal/Emitter/ModuleEmitter.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.VisualStudio.Adapter.Generator.Internal.Emitter; 2 | 3 | internal sealed class ModuleEmitter 4 | { 5 | private readonly ModuleBuilder _builder; 6 | 7 | public ModuleEmitter(ModuleBuilder builder) 8 | => _builder = builder; 9 | 10 | public Type Class(string fullName, Action emit) 11 | => Class(fullName, typeof(object), emit); 12 | 13 | public Type Class(string fullName, Type baseType, Action emit) 14 | { 15 | const TypeAttributes typeAttributes = 0 16 | | TypeAttributes.Public 17 | | TypeAttributes.Sealed 18 | | TypeAttributes.Class 19 | | TypeAttributes.BeforeFieldInit; 20 | 21 | baseType ??= typeof(object); 22 | 23 | var typeBuilder = _builder.DefineType(fullName, typeAttributes, baseType); 24 | 25 | emit(new ClassEmitter(typeBuilder)); 26 | 27 | return typeBuilder.CreateType(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Generator/Internal/Emitter/PropertyEmitter.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.Adapter.Generator.Internal.Utils; 2 | 3 | namespace ExtensionManager.VisualStudio.Adapter.Generator.Internal.Emitter; 4 | 5 | internal sealed class PropertyEmitter 6 | { 7 | private readonly ClassEmitter _typeEmitter; 8 | private readonly PropertyBuilder _builder; 9 | 10 | public PropertyEmitter(ClassEmitter typeEmitter, PropertyBuilder builder) 11 | { 12 | _typeEmitter = typeEmitter; 13 | _builder = builder; 14 | } 15 | 16 | public MethodBuilder GetField(FieldBuilder fieldBuilder, MethodAttributes attributes) 17 | { 18 | return Get(attributes) 19 | .EmitIL(il => 20 | { 21 | il.Emit(OpCodes.Ldarg_0); 22 | il.Emit(OpCodes.Ldfld, fieldBuilder); 23 | il.Emit(OpCodes.Ret); 24 | }); 25 | } 26 | 27 | public MethodBuilder SetField(FieldBuilder fieldBuilder, MethodAttributes attributes) 28 | { 29 | return Set(attributes) 30 | .EmitIL(il => 31 | { 32 | il.Emit(OpCodes.Ldarg_0); 33 | il.Emit(OpCodes.Ldarg_1); 34 | il.Emit(OpCodes.Stfld, fieldBuilder); 35 | il.Emit(OpCodes.Ret); 36 | }); 37 | } 38 | 39 | public MethodBuilder Get(MethodAttributes attributes) 40 | { 41 | var builder = _typeEmitter.Method( 42 | $"get_{_builder.Name}", 43 | attributes, 44 | _builder.PropertyType, 45 | []); 46 | 47 | _builder.SetGetMethod(builder); 48 | 49 | return builder; 50 | } 51 | 52 | public MethodBuilder Set(MethodAttributes attributes) 53 | { 54 | var builder = _typeEmitter.Method( 55 | $"set_{_builder.Name}", 56 | attributes, 57 | typeof(void), 58 | [_builder.PropertyType]); 59 | 60 | _builder.SetGetMethod(builder); 61 | 62 | return builder; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Generator/Internal/GeneratorContext.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.Adapter.Generator.Internal.Emitter; 2 | 3 | namespace ExtensionManager.VisualStudio.Adapter.Generator.Internal; 4 | 5 | internal sealed class GeneratorContext 6 | { 7 | public GeneratorReflector Reflect { get; } 8 | public ModuleEmitter Emit { get; } 9 | public string RootNamespace { get; } 10 | 11 | public GeneratorContext(IReadOnlyList assemblies, ModuleBuilder moduleBuilder, string rootNamespace) 12 | { 13 | Reflect = new(assemblies); 14 | Emit = new(moduleBuilder); 15 | RootNamespace = rootNamespace; 16 | } 17 | 18 | public Type EmitType(ITypeGenerator generator) 19 | => generator.Emit(this); 20 | } 21 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Generator/Internal/GeneratorReflector.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.Adapter.Generator.Internal.Utils; 2 | 3 | namespace ExtensionManager.VisualStudio.Adapter.Generator.Internal; 4 | 5 | internal sealed class GeneratorReflector 6 | { 7 | private const BindingFlags SearchBindingFlags = 0 8 | | BindingFlags.Public 9 | | BindingFlags.NonPublic 10 | | BindingFlags.Instance 11 | | BindingFlags.Static; 12 | 13 | private readonly IReadOnlyList _assemblies; 14 | 15 | public GeneratorReflector(IReadOnlyList assemblies) 16 | { 17 | _assemblies = assemblies; 18 | } 19 | 20 | public Type GetType(string fullName) 21 | { 22 | foreach (var assembly in _assemblies) 23 | { 24 | var type = assembly.GetType(fullName); 25 | 26 | if (type is not null) 27 | return type; 28 | } 29 | 30 | throw new TypeLoadException($"Type {fullName} not found"); 31 | } 32 | 33 | public MethodInfo GetMethod(Type type, string name) 34 | => (MethodInfo)GetMember(type, MemberTypes.Method, name); 35 | 36 | public PropertyInfo GetProperty(Type type, string name) 37 | => (PropertyInfo)GetMember(type, MemberTypes.Property, name); 38 | 39 | private MemberInfo GetMember(Type type, MemberTypes memberType, string name) 40 | { 41 | return type.GetMember(name, memberType, SearchBindingFlags).SingleOrDefault() 42 | ?? throw new TypeLoadException($"{memberType} {name} in {type} not found"); 43 | } 44 | 45 | public Type IInstalledExtension() => GetType("Microsoft.VisualStudio.ExtensionManager.IInstalledExtension"); 46 | public Type IVsExtensionManager() => GetType("Microsoft.VisualStudio.ExtensionManager.IVsExtensionManager"); 47 | public Type SVsExtensionManager() => GetType("Microsoft.VisualStudio.ExtensionManager.SVsExtensionManager"); 48 | public Type IVsExtensionRepository() => GetType("Microsoft.VisualStudio.ExtensionManager.IVsExtensionRepository"); 49 | public Type SVsExtensionRepository() => GetType("Microsoft.VisualStudio.ExtensionManager.SVsExtensionRepository"); 50 | 51 | public MethodInfo VS_GetRequiredServiceAsync(Type serviceType, Type interfaceType) 52 | { 53 | return GetType("Community.VisualStudio.Toolkit.VS") 54 | .GetMethodOrThrow("GetRequiredServiceAsync") 55 | .MakeGenericMethod(serviceType, interfaceType); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Generator/Internal/ITypeGenerator.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.VisualStudio.Adapter.Generator.Internal; 2 | 3 | internal interface ITypeGenerator 4 | { 5 | Type Emit(GeneratorContext context); 6 | } 7 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Generator/Internal/Utils/LinqExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.VisualStudio.Adapter.Generator.Internal.Utils; 2 | 3 | internal static class LinqExtensions 4 | { 5 | public static TResult[] SelectArray(this IReadOnlyList source, Func selector) 6 | { 7 | var result = new TResult[source.Count]; 8 | 9 | for (var i = 0; i < source.Count; i++) 10 | result[i] = selector(source[i]); 11 | 12 | return result; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Generator/Internal/Utils/ReflectionEmitExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.VisualStudio.Adapter.Generator.Internal.Utils; 2 | 3 | internal static class ReflectionEmitExtensions 4 | { 5 | public static MethodBuilder EmitIL(this MethodBuilder builder, Action generateIL) 6 | { 7 | generateIL(builder.GetILGenerator()); 8 | return builder; 9 | } 10 | 11 | public static void Throws(this MethodBuilder builder) 12 | where TException : Exception 13 | { 14 | builder.EmitIL(il => il 15 | .Emit(OpCodes.Throw, typeof(TException).GetConstructor(Type.EmptyTypes))); 16 | } 17 | 18 | public static ConstructorBuilder EmitIL(this ConstructorBuilder builder, Action generateIL) 19 | { 20 | generateIL(builder.GetILGenerator()); 21 | return builder; 22 | } 23 | 24 | public static void EmitCallEmptyBaseCtor(this ILGenerator il, Type baseType) 25 | { 26 | const BindingFlags searchCtorAttributes = 0 27 | | BindingFlags.Public 28 | | BindingFlags.NonPublic 29 | | BindingFlags.Instance; 30 | 31 | il.Emit(OpCodes.Ldarg_0); 32 | il.Emit(OpCodes.Call, baseType.GetConstructor(searchCtorAttributes, null, [], [])); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Generator/Internal/Utils/ReflectionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.VisualStudio.Adapter.Generator.Internal.Utils; 2 | 3 | internal static class ReflectionExtensions 4 | { 5 | public static MethodInfo GetMethodOrThrow(this Type type, string name) 6 | { 7 | return type.GetMethod(name) 8 | ?? throw new InvalidOperationException($"Method not found: {type.FullName}.{name}"); 9 | } 10 | 11 | public static MethodAttributes GetMethodAttributes(this MethodBase method) 12 | { 13 | var result = method switch 14 | { 15 | { IsPublic: true } => MethodAttributes.Public, 16 | { IsFamily: true } => MethodAttributes.Family, 17 | { IsAssembly: true } => MethodAttributes.Assembly, 18 | { IsFamilyAndAssembly: true } => MethodAttributes.FamANDAssem, 19 | { IsFamilyOrAssembly: true } => MethodAttributes.FamORAssem, 20 | { IsPrivate: true } => MethodAttributes.Private, 21 | _ => default 22 | }; 23 | 24 | if (method.DeclaringType.IsInterface) 25 | result |= MethodAttributes.NewSlot; 26 | 27 | return result 28 | | MethodAttributes.SpecialName 29 | | MethodAttributes.HideBySig 30 | | MethodAttributes.Virtual 31 | | MethodAttributes.Final; 32 | } 33 | 34 | public static PropertyInfo GetPropertyFlatten(this Type type, string name) 35 | { 36 | return GetPropertyFlattenOrNull(type, name) 37 | ?? throw new ArgumentException($"Property {name} of type {type} not found"); 38 | } 39 | 40 | private static PropertyInfo? GetPropertyFlattenOrNull(Type? type, string name) 41 | { 42 | if (type is null) 43 | return null; 44 | 45 | var property = type.GetProperty(name); 46 | 47 | if (property is not null) 48 | return property; 49 | 50 | foreach (var baseTypes in type.GetInterfaces()) 51 | { 52 | property = GetPropertyFlattenOrNull(baseTypes, name); 53 | 54 | if (property is not null) 55 | return property; 56 | } 57 | 58 | return null; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Generator/Types/AdapterServicesFactoryGenerator.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.Adapter.Extensions; 2 | using ExtensionManager.VisualStudio.Adapter.Generator.Internal; 3 | using ExtensionManager.VisualStudio.Adapter.Generator.Internal.Utils; 4 | 5 | namespace ExtensionManager.VisualStudio.Adapter.Generator.Types; 6 | 7 | internal sealed class AdapterServicesFactoryGenerator : ITypeGenerator 8 | { 9 | private readonly Type _genericManagerAdapterType; 10 | private readonly Type _genericRepositoryAdapterType; 11 | 12 | public AdapterServicesFactoryGenerator(Type genericManagerAdapterType, Type genericRepositoryAdapterType) 13 | { 14 | _genericManagerAdapterType = genericManagerAdapterType; 15 | _genericRepositoryAdapterType = genericRepositoryAdapterType; 16 | } 17 | 18 | public Type Emit(GeneratorContext context) 19 | { 20 | return context.Emit.Class($"{context.RootNamespace}.<>VSAdapterServicesFactory", 21 | emit => 22 | { 23 | emit.Implement( 24 | iEmit => 25 | { 26 | iEmit.Method(x => x.CreateExtensionManagerAdapter()) 27 | .EmitIL(il => 28 | { 29 | il.Emit(OpCodes.Newobj, _genericManagerAdapterType.GetConstructor([])); 30 | il.Emit(OpCodes.Newobj, context.Reflect.ManagerAdapterType().GetConstructor([context.Reflect.ManagerAdapterInterfaceType()])); 31 | il.Emit(OpCodes.Ret); 32 | }); 33 | 34 | iEmit.Method(x => x.CreateExtensionRepositoryAdapter()) 35 | .EmitIL(il => 36 | { 37 | il.Emit(OpCodes.Newobj, _genericRepositoryAdapterType.GetConstructor([])); 38 | il.Emit(OpCodes.Newobj, context.Reflect.RepositoryAdapterType().GetConstructor([context.Reflect.RepositoryAdapterInterfaceType()])); 39 | il.Emit(OpCodes.Ret); 40 | }); 41 | }); 42 | 43 | emit.DefaultCtor(); 44 | }); 45 | } 46 | } 47 | 48 | file static class Extensions 49 | { 50 | public static Type ManagerAdapterInterfaceType(this GeneratorReflector reflect) => typeof(IVSExtensionManagerAdapter<,>).MakeGenericType(reflect.IVsExtensionManager(), reflect.IInstalledExtension()); 51 | public static Type RepositoryAdapterInterfaceType(this GeneratorReflector reflect) => typeof(IVSExtensionRepositoryAdapter<>).MakeGenericType(reflect.IVsExtensionRepository()); 52 | 53 | public static Type ManagerAdapterType(this GeneratorReflector reflect) => typeof(VSExtensionManagerAdapter<,>).MakeGenericType(reflect.IVsExtensionManager(), reflect.IInstalledExtension()); 54 | public static Type RepositoryAdapterType(this GeneratorReflector reflect) => typeof(VSExtensionRepositoryAdapter<>).MakeGenericType(reflect.IVsExtensionRepository()); 55 | } 56 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Generator/Types/Extensions/ExtensionManagerAdapterGenerator.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.Adapter.Extensions; 2 | using ExtensionManager.VisualStudio.Adapter.Generator.Internal; 3 | using ExtensionManager.VisualStudio.Adapter.Generator.Internal.Utils; 4 | 5 | namespace ExtensionManager.VisualStudio.Adapter.Generator.Types.Extensions; 6 | 7 | internal sealed class ExtensionManagerAdapterGenerator : ITypeGenerator 8 | { 9 | private readonly Type _installedExtensionInfoType; 10 | 11 | public ExtensionManagerAdapterGenerator(Type installedExtensionInfoType) 12 | => _installedExtensionInfoType = installedExtensionInfoType; 13 | 14 | public Type Emit(GeneratorContext context) 15 | { 16 | return context.Emit.Class($"{context.RootNamespace}.Extensions.<>VSExtensionManagerAdapter", 17 | emit => 18 | { 19 | emit.Implement(context.Reflect.AdapterInterface()); 20 | 21 | emit.ImplementMethod(context.Reflect.Adapter_GetManagerAsync()) 22 | .EmitIL(il => 23 | { 24 | il.Emit(OpCodes.Call, context.Reflect.VS_GetManagerServiceAsync()); 25 | il.Emit(OpCodes.Ret); 26 | }); 27 | 28 | emit.ImplementMethod(context.Reflect.Adapter_GetInstalledExtensions()) 29 | .EmitIL(il => 30 | { 31 | il.Emit(OpCodes.Ldarg_1); 32 | il.Emit(OpCodes.Callvirt, context.Reflect.Manager_GetInstalledExtensions()); 33 | il.Emit(OpCodes.Ret); 34 | }); 35 | 36 | emit.ImplementMethod(context.Reflect.Adapter_CreateInstalledExtensionInfo()) 37 | .EmitIL(il => 38 | { 39 | il.Emit(OpCodes.Ldarg_1); 40 | il.Emit(OpCodes.Newobj, _installedExtensionInfoType.GetConstructor([context.Reflect.IInstalledExtension()])); 41 | il.Emit(OpCodes.Ret); 42 | }); 43 | 44 | emit.DefaultCtor(); 45 | }); 46 | } 47 | } 48 | 49 | file static class Extensions 50 | { 51 | public static Type AdapterInterface(this GeneratorReflector reflect) => typeof(IVSExtensionManagerAdapter<,>).MakeGenericType(reflect.IVsExtensionManager(), reflect.IInstalledExtension()); 52 | 53 | public static MethodInfo Adapter_GetManagerAsync(this GeneratorReflector reflect) => reflect.AdapterInterface().GetMethod(nameof(IVSExtensionManagerAdapter.GetManagerAsync)); 54 | public static MethodInfo Adapter_GetInstalledExtensions(this GeneratorReflector reflect) => reflect.AdapterInterface().GetMethod(nameof(IVSExtensionManagerAdapter.GetInstalledExtensions)); 55 | public static MethodInfo Adapter_CreateInstalledExtensionInfo(this GeneratorReflector reflect) => reflect.AdapterInterface().GetMethod(nameof(IVSExtensionManagerAdapter.CreateInstalledExtensionInfo)); 56 | 57 | public static MethodInfo VS_GetManagerServiceAsync(this GeneratorReflector reflect) => reflect.VS_GetRequiredServiceAsync(reflect.SVsExtensionManager(), reflect.IVsExtensionManager()); 58 | 59 | public static MethodInfo Manager_GetInstalledExtensions(this GeneratorReflector reflect) => reflect.IVsExtensionManager().GetMethodOrThrow("GetInstalledExtensions"); 60 | } 61 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Generator/Types/Extensions/ExtensionRepositoryAdapterGenerator.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.Adapter.Extensions; 2 | using ExtensionManager.VisualStudio.Adapter.Generator.Internal; 3 | using ExtensionManager.VisualStudio.Adapter.Generator.Internal.Utils; 4 | 5 | namespace ExtensionManager.VisualStudio.Adapter.Generator.Types.Extensions; 6 | 7 | internal sealed class ExtensionRepositoryAdapterGenerator : ITypeGenerator 8 | { 9 | private readonly Type _galleryExtensionType; 10 | 11 | public ExtensionRepositoryAdapterGenerator(Type galleryExtensionType) 12 | => _galleryExtensionType = galleryExtensionType; 13 | 14 | public Type Emit(GeneratorContext context) 15 | { 16 | return context.Emit.Class($"{context.RootNamespace}.Extensions.<>VSExtensionRepositoryAdapter", 17 | emit => 18 | { 19 | emit.Implement(context.Reflect.AdapterInterface()); 20 | 21 | emit.ImplementMethod(context.Reflect.Adapter_GetRepositoryAsync()) 22 | .EmitIL(il => 23 | { 24 | il.Emit(OpCodes.Call, context.Reflect.VS_GetRepositoryServiceAsync()); 25 | il.Emit(OpCodes.Ret); 26 | }); 27 | 28 | emit.ImplementMethod(context.Reflect.Adapter_GetVSGalleryExtensions()) 29 | .EmitIL(il => 30 | { 31 | il.Emit(OpCodes.Ldarg_1); 32 | il.Emit(OpCodes.Ldarg_2); 33 | il.Emit(OpCodes.Ldarg_3); 34 | il.Emit(OpCodes.Ldarg, 4); 35 | il.Emit(OpCodes.Callvirt, context.Reflect.Repository_GetVSGalleryExtensions(_galleryExtensionType)); 36 | il.Emit(OpCodes.Ret); 37 | }); 38 | 39 | emit.DefaultCtor(); 40 | }); 41 | } 42 | } 43 | 44 | file static class Extensions 45 | { 46 | public static Type AdapterInterface(this GeneratorReflector reflect) => typeof(IVSExtensionRepositoryAdapter<>).MakeGenericType(reflect.IVsExtensionRepository()); 47 | 48 | public static MethodInfo Adapter_GetRepositoryAsync(this GeneratorReflector reflect) => reflect.AdapterInterface().GetMethod(nameof(IVSExtensionRepositoryAdapter.GetRepositoryAsync)); 49 | public static MethodInfo Adapter_GetVSGalleryExtensions(this GeneratorReflector reflect) => reflect.AdapterInterface().GetMethod(nameof(IVSExtensionRepositoryAdapter.GetVSGalleryExtensions)); 50 | 51 | public static MethodInfo VS_GetRepositoryServiceAsync(this GeneratorReflector reflect) => reflect.VS_GetRequiredServiceAsync(reflect.SVsExtensionRepository(), reflect.IVsExtensionRepository()); 52 | 53 | public static MethodInfo Repository_GetVSGalleryExtensions(this GeneratorReflector reflect, Type extensionType) => reflect.IVsExtensionRepository().GetMethodOrThrow("GetVSGalleryExtensions").MakeGenericMethod(extensionType); 54 | } 55 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Generator/Types/Extensions/GalleryExtensionGenerator.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.Adapter.Generator.Internal; 2 | using ExtensionManager.VisualStudio.Adapter.Generator.Internal.Emitter; 3 | using ExtensionManager.VisualStudio.Adapter.Generator.Internal.Utils; 4 | using ExtensionManager.VisualStudio.Extensions; 5 | 6 | namespace ExtensionManager.VisualStudio.Adapter.Generator.Types.Extensions; 7 | 8 | internal sealed class GalleryExtensionGenerator : ITypeGenerator 9 | { 10 | private readonly string _baseTypeName; 11 | 12 | public GalleryExtensionGenerator(string baseTypeName) 13 | => _baseTypeName = baseTypeName; 14 | 15 | public Type Emit(GeneratorContext context) 16 | { 17 | const BindingFlags searchBindingFlags = 0 18 | | BindingFlags.Public 19 | | BindingFlags.NonPublic 20 | | BindingFlags.Instance 21 | | BindingFlags.FlattenHierarchy; 22 | 23 | var baseType = context.Reflect.GetType($"Microsoft.VisualStudio.ExtensionManager.{_baseTypeName}"); 24 | 25 | return context.Emit.Class($"{context.RootNamespace}.Extensions.<>VSGalleryExtension", baseType, emit => 26 | { 27 | emit.Implement(typeof(IVSExtension)); 28 | 29 | var properties = baseType.GetProperties(searchBindingFlags); 30 | 31 | foreach (var property in properties) 32 | emit.OverrideProperty(property); 33 | 34 | foreach (var method in baseType.GetMethods(searchBindingFlags)) 35 | { 36 | if (properties.Any(x => x.GetMethod == method || x.SetMethod == method)) 37 | continue; 38 | 39 | emit.OverrideMethod(method); 40 | } 41 | 42 | emit.DefaultCtor(); 43 | }); 44 | } 45 | } 46 | 47 | file static class Extensions 48 | { 49 | public static void OverrideProperty(this ClassEmitter classEmitter, PropertyInfo property) 50 | { 51 | var isAbstract = (property.GetMethod ?? property.SetMethod)?.IsAbstract ?? false; 52 | 53 | if (!isAbstract) 54 | return; 55 | 56 | if (property.GetMethod is not null && property.SetMethod is not null) 57 | { 58 | var fieldBuilder = classEmitter.Field($"<{property.Name}>k__BackingField", property.PropertyType, FieldAttributes.Private | FieldAttributes.InitOnly); 59 | 60 | classEmitter.Property(property.Name, property.PropertyType, 61 | emit => 62 | { 63 | emit.GetField(fieldBuilder, property.GetMethod.GetMethodAttributes()); 64 | emit.SetField(fieldBuilder, property.SetMethod.GetMethodAttributes()); 65 | }); 66 | } 67 | else 68 | { 69 | classEmitter.Property(property.Name, property.PropertyType, 70 | emit => 71 | { 72 | if (property.GetMethod is not null) 73 | emit.Get(property.GetMethod.GetMethodAttributes()).Throws(); 74 | 75 | if (property.SetMethod is not null) 76 | emit.Set(property.SetMethod.GetMethodAttributes()).Throws(); 77 | }); 78 | } 79 | } 80 | 81 | public static void OverrideMethod(this ClassEmitter classEmitter, MethodInfo method) 82 | { 83 | if (!method.IsAbstract) 84 | return; 85 | 86 | var methodAttributes = method.GetMethodAttributes(); 87 | var parameterTypes = method.GetParameters().SelectArray(p => p.ParameterType); 88 | 89 | classEmitter 90 | .Method(method.Name, methodAttributes, method.ReturnType, parameterTypes) 91 | .Throws(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Generator/Types/Extensions/InstalledExtensionInfoGenerator.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.Adapter.Extensions; 2 | using ExtensionManager.VisualStudio.Adapter.Generator.Internal; 3 | using ExtensionManager.VisualStudio.Adapter.Generator.Internal.Utils; 4 | 5 | namespace ExtensionManager.VisualStudio.Adapter.Generator.Types.Extensions; 6 | 7 | internal sealed class InstalledExtensionInfoGenerator : ITypeGenerator 8 | { 9 | public Type Emit(GeneratorContext context) 10 | { 11 | return context.Emit.Class($"{context.RootNamespace}.Extensions.<>InstalledExtensionInfo", 12 | emit => 13 | { 14 | var fieldBuilder = emit.Field($"<>_extension", context.Reflect.IInstalledExtension(), FieldAttributes.Private | FieldAttributes.InitOnly); 15 | 16 | emit.Implement( 17 | iEmit => 18 | { 19 | iEmit.Property(x => x.Identifier, 20 | (pEmit, p) => pEmit.Get(p.GetMethod.GetMethodAttributes()) 21 | .EmitIL(il => 22 | { 23 | il.Emit(OpCodes.Ldarg_0); 24 | il.Emit(OpCodes.Ldfld, fieldBuilder); 25 | il.Emit(OpCodes.Callvirt, context.Reflect.InstalledExtension_Header().GetMethod); 26 | il.Emit(OpCodes.Callvirt, context.Reflect.InstalledExtension_Header_Identifier().GetMethod); 27 | il.Emit(OpCodes.Ret); 28 | })); 29 | 30 | iEmit.Property(x => x.IsSystemComponent, 31 | (pEmit, p) => pEmit.Get(p.GetMethod.GetMethodAttributes()) 32 | .EmitIL(il => 33 | { 34 | il.Emit(OpCodes.Ldarg_0); 35 | il.Emit(OpCodes.Ldfld, fieldBuilder); 36 | il.Emit(OpCodes.Callvirt, context.Reflect.InstalledExtension_Header().GetMethod); 37 | il.Emit(OpCodes.Callvirt, context.Reflect.InstalledExtension_Header_SystemComponent().GetMethod); 38 | il.Emit(OpCodes.Ret); 39 | })); 40 | 41 | iEmit.Property(x => x.IsPackComponent, 42 | (pEmit, p) => pEmit.Get(p.GetMethod.GetMethodAttributes()) 43 | .EmitIL(il => 44 | { 45 | il.Emit(OpCodes.Ldarg_0); 46 | il.Emit(OpCodes.Ldfld, fieldBuilder); 47 | il.Emit(OpCodes.Callvirt, context.Reflect.InstalledExtension_IsPackComponent().GetMethod); 48 | il.Emit(OpCodes.Ret); 49 | })); 50 | }); 51 | 52 | emit.AutoCtor(fieldBuilder); 53 | }); 54 | } 55 | } 56 | 57 | file static class Extensions 58 | { 59 | public static PropertyInfo InstalledExtension_Header(this GeneratorReflector reflect) => reflect.IInstalledExtension().GetPropertyFlatten("Header"); 60 | public static PropertyInfo InstalledExtension_Header_Identifier(this GeneratorReflector reflect) => reflect.InstalledExtension_Header().PropertyType.GetPropertyFlatten("Identifier"); 61 | public static PropertyInfo InstalledExtension_Header_SystemComponent(this GeneratorReflector reflect) => reflect.InstalledExtension_Header().PropertyType.GetPropertyFlatten("SystemComponent"); 62 | public static PropertyInfo InstalledExtension_IsPackComponent(this GeneratorReflector reflect) => reflect.IInstalledExtension().GetPropertyFlatten("IsPackComponent"); 63 | } 64 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.Generator/VSAdapterServicesFactoryGeneratorBase.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.Adapter.Generator.Internal; 2 | using ExtensionManager.VisualStudio.Adapter.Generator.Types; 3 | using ExtensionManager.VisualStudio.Adapter.Generator.Types.Extensions; 4 | 5 | namespace ExtensionManager.VisualStudio.Adapter.Generator; 6 | 7 | public abstract class VSAdapterServicesFactoryGeneratorBase 8 | { 9 | private readonly Version _visualStudioVersion; 10 | 11 | public VSAdapterServicesFactoryGeneratorBase(Version visualStudioVersion) 12 | => _visualStudioVersion = visualStudioVersion; 13 | 14 | public IVSAdapterServicesFactory Generate() 15 | => (IVSAdapterServicesFactory)Activator.CreateInstance(GenerateAdapterServicesFactoryType()); 16 | 17 | private Type GenerateAdapterServicesFactoryType() 18 | { 19 | var rootNamespace = typeof(IVSAdapterServicesFactory).Namespace; 20 | var assemblyName = $"{rootNamespace}.{_visualStudioVersion}"; 21 | 22 | var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new(assemblyName), AssemblyBuilderAccess.Run); 23 | var moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName); 24 | 25 | var assemblies = LoadVisualStudioAssemblies() 26 | .Concat(AppDomain.CurrentDomain.GetAssemblies()) 27 | .ToList(); 28 | 29 | var context = new GeneratorContext(assemblies, moduleBuilder, rootNamespace); 30 | 31 | var galleryExtensionType = context.EmitType(new GalleryExtensionGenerator(GetGalleryExtensionBaseTypeName())); 32 | var installedExtensionInfoType = context.EmitType(new InstalledExtensionInfoGenerator()); 33 | var managerAdapterType = context.EmitType(new ExtensionManagerAdapterGenerator(installedExtensionInfoType)); 34 | var repositoryAdapterType = context.EmitType(new ExtensionRepositoryAdapterGenerator(galleryExtensionType)); 35 | 36 | return context.EmitType(new AdapterServicesFactoryGenerator(managerAdapterType, repositoryAdapterType)); 37 | } 38 | 39 | protected abstract string GetGalleryExtensionBaseTypeName(); 40 | protected abstract IEnumerable LoadVisualStudioAssemblies(); 41 | } 42 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Adapter.md: -------------------------------------------------------------------------------- 1 | # ExtensionManager.VisualStudio.Adapter 2 | 3 | The primary goal of these projects is to ensure seamless compatibility with various versions and updates of Visual Studio. 4 | This aims to reduce the need for frequent updates and maintenance. 5 | 6 | ## Problems & Solutions 7 | 8 | Different versions of Visual Studio, including full and preview versions, come with different internal DLLs. 9 | Since these DLLs are part of active development, they can vary between versions, posing a challenge not only for supporting the latest updates but also for older versions. 10 | 11 | ## Solution 12 | 13 | The solution relies on generating IL code to create the necessary code according to the differences in various Visual Studio versions. 14 | This way, the required DLLs only need to be searched for and loaded when the extension is loaded, 15 | eliminating the need for manual updates and making the extension compatible with older updates as well. 16 | 17 | It is crucial that the abstraction is as simple as possible to minimize the complexity of the IL code to be generated. 18 | 19 | ## Projects 20 | 21 | | Project | Description | 22 | |---|---| 23 | | ExtensionManager.VisualStudio.Adapter.Abstractions | Contains the abstractions for IL generation. | 24 | | ExtensionManager.VisualStudio.Adapter.Generator | Contains classes for generating the specific code. | 25 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Shared/Documents/VSDocuments.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using Community.VisualStudio.Toolkit; 4 | 5 | #nullable enable 6 | 7 | namespace ExtensionManager.VisualStudio.Documents; 8 | 9 | internal sealed class VSDocuments : IVSDocuments 10 | { 11 | public Task OpenAsync(string file) => VS.Documents.OpenAsync(file); 12 | } 13 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Shared/ExtensionManager.VisualStudio.Shared.projitems: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | true 6 | db482bd0-94ba-4e7d-8a51-ac2a2663267a 7 | 8 | 9 | ExtensionManager.VisualStudio 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Shared/ExtensionManager.VisualStudio.Shared.shproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | db482bd0-94ba-4e7d-8a51-ac2a2663267a 5 | 14.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Shared/Extensions/VSExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | using Community.VisualStudio.Toolkit; 8 | 9 | using ExtensionManager.VisualStudio.Adapter.Extensions; 10 | 11 | using Microsoft.VisualStudio.Setup.Configuration; 12 | 13 | #nullable enable 14 | 15 | namespace ExtensionManager.VisualStudio.Extensions; 16 | 17 | internal sealed class VSExtensions : IVSExtensions 18 | { 19 | private readonly IVSExtensionRepositoryAdapter _repositoryAdapter; 20 | private readonly IVSExtensionManagerAdapter _managerAdapter; 21 | 22 | public VSExtensions(IVSExtensionRepositoryAdapter repositoryAdapter, IVSExtensionManagerAdapter managerAdapter) 23 | { 24 | _repositoryAdapter = repositoryAdapter; 25 | _managerAdapter = managerAdapter; 26 | } 27 | 28 | public async Task> GetGalleryExtensionsAsync(IEnumerable extensionIds) 29 | { 30 | var extensionIdsList = extensionIds as List ?? extensionIds.ToList(); 31 | 32 | return await _repositoryAdapter.GetVSGalleryExtensionsAsync(extensionIdsList, 1033, false); 33 | } 34 | 35 | public async Task> GetInstalledExtensionsAsync() 36 | { 37 | var installedExtensions = await _managerAdapter.GetInstalledExtensionsAsync(); 38 | 39 | var extensionIds = installedExtensions 40 | .Where(i => !i.IsSystemComponent) 41 | .Where(i => !i.IsPackComponent) 42 | .Select(i => i.Identifier) 43 | .ToList(); 44 | 45 | return await GetGalleryExtensionsAsync(extensionIds); 46 | } 47 | 48 | public async Task StartInstallerAsync(IEnumerable vsixFiles, bool systemWide) 49 | { 50 | var rootSuffix = await VS.Shell.TryGetCommandLineArgumentAsync("rootsuffix").ConfigureAwait(false); 51 | 52 | vsixFiles = vsixFiles.Select(x => $"\"{x}\""); 53 | 54 | var arguments = $"{string.Join(" ", vsixFiles)} /instanceIds:{GetInstallationId()}"; 55 | 56 | if (systemWide) 57 | arguments += $" /admin"; 58 | 59 | if (!string.IsNullOrEmpty(rootSuffix)) 60 | arguments += $" /rootSuffix:{rootSuffix}"; 61 | 62 | Process.Start(new ProcessStartInfo 63 | { 64 | FileName = GetVsixInstallerFilePath(), 65 | Arguments = arguments, 66 | UseShellExecute = false, 67 | }); 68 | 69 | static string GetInstallationId() 70 | { 71 | return ((ISetupConfiguration)new SetupConfiguration()) 72 | .GetInstanceForCurrentProcess() 73 | .GetInstanceId(); 74 | } 75 | 76 | static string GetVsixInstallerFilePath() 77 | { 78 | var process = Process.GetCurrentProcess(); 79 | var dir = Path.GetDirectoryName(process.MainModule.FileName); 80 | return Path.Combine(dir, "VSIXInstaller.exe"); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Shared/MessageBox/VSMessageBox.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using Community.VisualStudio.Toolkit; 4 | 5 | using Microsoft.VisualStudio; 6 | 7 | #nullable enable 8 | 9 | namespace ExtensionManager.VisualStudio.MessageBox; 10 | 11 | internal sealed class VSMessageBox : IVSMessageBox 12 | { 13 | public Task ShowErrorAsync(string line1, string line2 = "") => VS.MessageBox.ShowErrorAsync(line1, line2); 14 | 15 | public async Task ShowWarningAsync(string line1, string line2 = "") 16 | { 17 | var result = await VS.MessageBox.ShowWarningAsync(line1, line2).ConfigureAwait(false); 18 | 19 | return result == VSConstants.MessageBoxResult.IDOK; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Shared/Solution/VSSolution.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using CT = Community.VisualStudio.Toolkit; 4 | 5 | #nullable enable 6 | 7 | namespace ExtensionManager.VisualStudio.Solution; 8 | 9 | internal sealed class VSSolution : VSSolutionItem, IVSSolution 10 | { 11 | public VSSolution(CT.Solution solution) 12 | : base(solution) 13 | { 14 | } 15 | 16 | public async Task AddSolutionFolderAsync(string name) 17 | { 18 | var folder = await Inner.AddSolutionFolderAsync(name).ConfigureAwait(false); 19 | 20 | if (folder is null) 21 | return null; 22 | 23 | return new VSSolutionFolder(folder); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Shared/Solution/VSSolutionFolder.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using CT = Community.VisualStudio.Toolkit; 4 | 5 | #nullable enable 6 | 7 | namespace ExtensionManager.VisualStudio.Solution; 8 | 9 | internal sealed class VSSolutionFolder : VSSolutionItem, IVSSolutionFolder 10 | { 11 | public VSSolutionFolder(CT.SolutionFolder folder) 12 | : base(folder) 13 | { 14 | } 15 | 16 | public Task AddExistingFilesAsync(params string[] files) => Inner.AddExistingFilesAsync(files); 17 | } 18 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Shared/Solution/VSSolutionItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | using CT = Community.VisualStudio.Toolkit; 7 | using ThreadHelper = Microsoft.VisualStudio.Shell.ThreadHelper; 8 | 9 | #nullable enable 10 | 11 | namespace ExtensionManager.VisualStudio.Solution; 12 | 13 | internal class VSSolutionItem : IVSSolutionItem 14 | where TItem : CT.SolutionItem 15 | { 16 | protected TItem Inner { get; } 17 | 18 | public string Name => Inner.Name; 19 | public string? FullPath => Inner.FullPath; 20 | 21 | protected VSSolutionItem(TItem inner) 22 | => Inner = inner; 23 | 24 | public async Task> GetChildrenAsync() 25 | { 26 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 27 | 28 | return Inner.Children 29 | .Where(x => x is not null) 30 | .Select(x => (IVSSolutionItem)(x switch 31 | { 32 | CT.Solution solution => new VSSolution(solution), 33 | CT.SolutionFolder folder => new VSSolutionFolder(folder), 34 | CT.SolutionItem item => new VSSolutionItem(item), 35 | _ => throw new NotImplementedException($"The solution item of type {x!.GetType()} is not supported"), 36 | })) 37 | .ToList(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Shared/Solution/VSSolutions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using VS = Community.VisualStudio.Toolkit.VS; 4 | 5 | #nullable enable 6 | 7 | namespace ExtensionManager.VisualStudio.Solution; 8 | 9 | internal sealed class VSSolutions : IVSSolutions 10 | { 11 | public Task IsOpenAsync() => VS.Solutions.IsOpenAsync(); 12 | 13 | public async Task GetCurrentSolutionAsync() 14 | { 15 | var solution = await VS.Solutions.GetCurrentSolutionAsync().ConfigureAwait(false); 16 | 17 | if (solution is null) 18 | return null; 19 | 20 | return new VSSolution(solution); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Shared/StatusBar/VSStatusBar.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using VS = Community.VisualStudio.Toolkit.VS; 4 | 5 | #nullable enable 6 | 7 | namespace ExtensionManager.VisualStudio.StatusBar; 8 | 9 | internal sealed class VSStatusBar : IVSStatusBar 10 | { 11 | public Task ClearAsync() => VS.StatusBar.ClearAsync(); 12 | public Task ShowMessageAsync(string text) => VS.StatusBar.ShowMessageAsync(text); 13 | public Task ShowProgressAsync(string text, int currentStep, int numberOfSteps) => VS.StatusBar.ShowProgressAsync(text, currentStep, numberOfSteps); 14 | } 15 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Shared/Themes/VSThemes.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | using CT = Community.VisualStudio.Toolkit; 4 | 5 | #nullable enable 6 | 7 | namespace ExtensionManager.VisualStudio.Themes; 8 | 9 | internal sealed class VSThemes : IVSThemes 10 | { 11 | public void Use(UIElement element, bool use = true) => CT.Themes.SetUseVsTheme(element, use); 12 | } 13 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Shared/Threads/VSThreads.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | using ThreadHelper = Microsoft.VisualStudio.Shell.ThreadHelper; 5 | 6 | #nullable enable 7 | 8 | namespace ExtensionManager.VisualStudio.Threads; 9 | 10 | internal sealed class VSThreads : IVSThreads 11 | { 12 | public bool CheckUIThreadAccess() 13 | => ThreadHelper.CheckAccess(); 14 | 15 | public async Task RunOnUIThreadAsync(Action syncMethod) 16 | { 17 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 18 | 19 | syncMethod(); 20 | } 21 | 22 | public async Task RunOnUIThreadAsync(Func syncMethod) 23 | { 24 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 25 | 26 | return syncMethod(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Shared/VSAdapterServicesFactoryGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | 5 | using ExtensionManager.VisualStudio.Adapter.Generator; 6 | 7 | #nullable enable 8 | 9 | namespace ExtensionManager.VisualStudio; 10 | 11 | internal sealed class VSAdapterServicesFactoryGenerator : VSAdapterServicesFactoryGeneratorBase 12 | { 13 | public VSAdapterServicesFactoryGenerator(Version visualStudioVersion) 14 | : base(visualStudioVersion) 15 | { 16 | } 17 | 18 | protected override string GetGalleryExtensionBaseTypeName() 19 | { 20 | #if VS2017 || VS2019 21 | return "GalleryOnlineExtension"; 22 | #elif VS2022 23 | return "OnlineExtensionBase"; 24 | #else 25 | #error Not implemented 26 | #endif 27 | } 28 | 29 | protected override IEnumerable LoadVisualStudioAssemblies() 30 | { 31 | var devenvDirectory = Environment.GetEnvironmentVariable("VSAPPIDDIR"); 32 | 33 | foreach (var filePath in GetVisualStudioAssemblyFilePaths(devenvDirectory)) 34 | yield return Assembly.LoadFile(filePath); 35 | } 36 | 37 | private IEnumerable GetVisualStudioAssemblyFilePaths(string devenvDirectory) 38 | { 39 | #if VS2017 || VS2019 40 | yield return @$"{devenvDirectory}Microsoft.VisualStudio.ExtensionEngine.dll"; 41 | yield return @$"{devenvDirectory}PrivateAssemblies\Microsoft.VisualStudio.ExtensionManager.dll"; 42 | yield return @$"{devenvDirectory}PrivateAssemblies\Microsoft.VisualStudio.ExtensionsExplorer.dll"; 43 | #elif VS2022 44 | yield return @$"{devenvDirectory}Microsoft.VisualStudio.ExtensionEngine.dll"; 45 | yield return @$"{devenvDirectory}Microsoft.VisualStudio.ExtensionEngineContract.dll"; 46 | yield return @$"{devenvDirectory}PrivateAssemblies\Microsoft.VisualStudio.ExtensionManager.dll"; 47 | yield return @$"{devenvDirectory}PrivateAssemblies\Microsoft.VisualStudio.ExtensionsExplorer.dll"; 48 | #else 49 | #error Not implemented 50 | #endif 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.Shared/VSServicesRegistrar.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using ExtensionManager.VisualStudio.Adapter; 4 | using ExtensionManager.VisualStudio.Documents; 5 | using ExtensionManager.VisualStudio.Extensions; 6 | using ExtensionManager.VisualStudio.MessageBox; 7 | using ExtensionManager.VisualStudio.Solution; 8 | using ExtensionManager.VisualStudio.StatusBar; 9 | using ExtensionManager.VisualStudio.Themes; 10 | using ExtensionManager.VisualStudio.Threads; 11 | 12 | using Microsoft.Extensions.DependencyInjection; 13 | 14 | #nullable enable 15 | 16 | #if VS2017 17 | namespace ExtensionManager.VisualStudio.VS2017; 18 | #elif VS2019 19 | namespace ExtensionManager.VisualStudio.VS2019; 20 | #elif VS2022 21 | namespace ExtensionManager.VisualStudio.VS2022; 22 | #else 23 | #error Not implemented 24 | #endif 25 | 26 | public sealed class VSServicesRegistrar : IVSServicesRegistrar 27 | { 28 | private readonly Version _visualStudioVersion; 29 | 30 | public VSServicesRegistrar(Version visualStudioVersion) 31 | => _visualStudioVersion = visualStudioVersion; 32 | 33 | public void AddServices(IServiceCollection services) 34 | { 35 | services.AddSingleton(new VSAdapterServicesFactoryGenerator(_visualStudioVersion).Generate()); 36 | services.AddSingleton(s => s.GetRequiredService().CreateExtensionManagerAdapter()); 37 | services.AddSingleton(s => s.GetRequiredService().CreateExtensionRepositoryAdapter()); 38 | 39 | services.AddSingleton(); 40 | services.AddSingleton(); 41 | services.AddSingleton(); 42 | services.AddSingleton(); 43 | services.AddSingleton(); 44 | services.AddSingleton(); 45 | services.AddSingleton(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ExtensionManager.VisualStudio.md: -------------------------------------------------------------------------------- 1 | # ExtensionManager.VisualStudio 2 | 3 | These projects provide a facade around the Visual Studio API, streamlining the handling of different dependency versions. 4 | 5 | ## Proble & Solution 6 | 7 | #### Version conflicts 8 | 9 | Different major versions of Visual Studio require specific dependencies that may not be compatible with each other. 10 | 11 | To manage this, an Abstractions project is created to define interfaces, which are then implemented in the various Vsix projects. 12 | The implementation code of these abstractions is housed in a Shared project. This approach allows normal SDK projects to utilize the 13 | functionality without needing to directly manage the dependencies for each Visual Studio version. 14 | 15 | ## Projects 16 | 17 | | Project | Description | 18 | |---|---| 19 | | ExtensionManager.VisualStudio.Abstractions | Houses the facade abstraction | 20 | | ExtensionManager.VisualStudio.Shared | Contains the implementation of the abstractions | 21 | -------------------------------------------------------------------------------- /src/ExtensionManager.Vsix.Shared/ExtensionManager.Vsix.Shared.projitems: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | true 6 | 77052739-b59a-4e8b-9e4a-8d1372f0e6b5 7 | 8 | 9 | ExtensionManager 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/ExtensionManager.Vsix.Shared/ExtensionManager.Vsix.Shared.shproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 77052739-b59a-4e8b-9e4a-8d1372f0e6b5 5 | 14.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/ExtensionManager.Vsix.Shared/ExtensionManagerPackage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.Design; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.IO; 5 | using System.Reflection; 6 | using System.Runtime.InteropServices; 7 | using System.Threading; 8 | 9 | using Community.VisualStudio.Toolkit; 10 | 11 | using ExtensionManager.Features.Export; 12 | using ExtensionManager.Features.Install; 13 | using ExtensionManager.UI; 14 | using ExtensionManager.VisualStudio; 15 | using ExtensionManager.VisualStudio.Solution; 16 | 17 | using Microsoft.Extensions.DependencyInjection; 18 | using Microsoft.VisualStudio; 19 | using Microsoft.VisualStudio.Shell; 20 | 21 | using ShellSolutionEvents = Microsoft.VisualStudio.Shell.Events.SolutionEvents; 22 | using Task = System.Threading.Tasks.Task; 23 | 24 | #nullable enable 25 | 26 | namespace ExtensionManager; 27 | 28 | [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] 29 | [InstalledProductRegistration(Vsix.Name, Vsix.Description, Vsix.Version)] 30 | [ProvideMenuResource("Menus.ctmenu", 1)] 31 | [Guid(PackageGuids.guidVsPackageString)] 32 | [ProvideAutoLoad(VSConstants.UICONTEXT.SolutionOpening_string, PackageAutoLoadFlags.BackgroundLoad)] 33 | public sealed class ExtensionManagerPackage : AsyncPackage 34 | { 35 | static ExtensionManagerPackage() 36 | => AssemblyResolver.Initialize(); 37 | 38 | // If a file is displayed by exporting the extensions, an empty solution is opened. 39 | // In this case, InstallSolutionExtensionsOnIdleAsync would be called, but this leads to an error because the solution has not yet been saved. 40 | private bool _isFeatureExecuting; 41 | 42 | protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) 43 | { 44 | var vsVersion = await VS.Shell.GetVsVersionAsync() 45 | ?? throw new InvalidOperationException("Cannot find running visual studio version"); 46 | 47 | var services = new ServiceCollection() 48 | .ConfigureVSServices(CreateVSServiceFactory(vsVersion)) 49 | .ConfigureExtensionManager(new ThisVsixInfo()) 50 | .BuildServiceProvider(); 51 | 52 | UIMarkupServices.Initialize(services); 53 | 54 | var solutions = services.GetRequiredService(); 55 | var featureExecutor = services.GetRequiredService(); 56 | 57 | await InitMenuCommandsAsync(featureExecutor); 58 | await HandleSolutionExtensionsAsync(solutions, featureExecutor); 59 | } 60 | 61 | private static IVSServicesRegistrar CreateVSServiceFactory(Version vsVersion) 62 | { 63 | #if VS2017 64 | return new VisualStudio.VS2017.VSServicesRegistrar(vsVersion); 65 | #elif VS2019 66 | return new VisualStudio.VS2019.VSServicesRegistrar(vsVersion); 67 | #elif VS2022 68 | return new VisualStudio.VS2022.VSServicesRegistrar(vsVersion); 69 | #else 70 | #error Not implemented 71 | #endif 72 | } 73 | 74 | private async Task HandleSolutionExtensionsAsync(IVSSolutions solutions, IFeatureExecutor executor) 75 | { 76 | if (await solutions.IsOpenAsync()) 77 | InstallSolutionExtensionsOnIdle(executor); 78 | 79 | ShellSolutionEvents.OnAfterOpenSolution += (s, e) => 80 | { 81 | if (_isFeatureExecuting) 82 | return; 83 | 84 | InstallSolutionExtensionsOnIdle(executor); 85 | }; 86 | 87 | void InstallSolutionExtensionsOnIdle(IFeatureExecutor executor) 88 | { 89 | JoinableTaskFactory 90 | .StartOnIdle(executor.ExecuteAsync) 91 | .FileAndForget($"{nameof(ExtensionManager)}/{nameof(InstallForSolutionFeature)}"); 92 | } 93 | } 94 | 95 | private async Task InitMenuCommandsAsync(IFeatureExecutor executor) 96 | { 97 | if (await GetServiceAsync(typeof(IMenuCommandService)) is not IMenuCommandService commandService) 98 | return; 99 | 100 | AddMenuCommand(executor, commandService, PackageGuids.guidExportPackageCmdSet, PackageIds.ExportCmd); 101 | AddMenuCommand(executor, commandService, PackageGuids.guidExportPackageCmdSet, PackageIds.ExportSolutionCmd); 102 | AddMenuCommand(executor, commandService, PackageGuids.guidExportPackageCmdSet, PackageIds.ImportCmd); 103 | } 104 | private void AddMenuCommand(IFeatureExecutor executor, IMenuCommandService commandService, Guid menuGroup, int commandID) 105 | where TFeature : class, IFeature 106 | { 107 | var cmdId = new CommandID(menuGroup, commandID); 108 | var cmd = new MenuCommand(OnHandleCommand, cmdId); 109 | commandService.AddCommand(cmd); 110 | 111 | [SuppressMessage("Usage", "VSTHRD100:Avoid async void methods")] 112 | async void OnHandleCommand(object sender, EventArgs e) 113 | { 114 | _isFeatureExecuting = true; 115 | 116 | try 117 | { 118 | await executor.ExecuteAsync(); 119 | } 120 | finally 121 | { 122 | _isFeatureExecuting = false; 123 | } 124 | } 125 | } 126 | } 127 | 128 | file static class AssemblyResolver 129 | { 130 | public static void Initialize() 131 | => AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve; 132 | 133 | private static Assembly? OnAssemblyResolve(object sender, ResolveEventArgs e) 134 | { 135 | var assemblyName = new AssemblyName(e.Name).Name; 136 | var currentFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); 137 | var assemblyPath = Path.Combine(currentFolder, assemblyName + ".dll"); 138 | 139 | if (File.Exists(assemblyPath)) 140 | return Assembly.LoadFile(assemblyPath); 141 | 142 | return null; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/ExtensionManager.Vsix.Shared/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | using ExtensionManager; 5 | 6 | [assembly: AssemblyTitle(Vsix.Name)] 7 | [assembly: AssemblyDescription(Vsix.Description)] 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany(Vsix.Author)] 10 | [assembly: AssemblyProduct(Vsix.Name)] 11 | [assembly: AssemblyCopyright(Vsix.Author)] 12 | [assembly: AssemblyTrademark("")] 13 | [assembly: AssemblyCulture("")] 14 | 15 | [assembly: ComVisible(false)] 16 | 17 | [assembly: AssemblyVersion(Vsix.Version)] 18 | [assembly: AssemblyFileVersion(Vsix.Version)] 19 | -------------------------------------------------------------------------------- /src/ExtensionManager.Vsix.Shared/ThisVsixInfo.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager; 2 | 3 | public sealed class ThisVsixInfo : IThisVsixInfo 4 | { 5 | public string Id => Vsix.Id; 6 | public string Name => Vsix.Name; 7 | public string Description => Vsix.Description; 8 | public string Language => Vsix.Language; 9 | public string Version => Vsix.Version; 10 | public string Author => Vsix.Author; 11 | public string Tags => Vsix.Tags; 12 | } 13 | -------------------------------------------------------------------------------- /src/ExtensionManager.Vsix.VS2017/VsComandTable.cs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // 3 | // This file was generated by VSIX Synchronizer 4 | // 5 | // ------------------------------------------------------------------------------ 6 | namespace ExtensionManager 7 | { 8 | using System; 9 | 10 | /// 11 | /// Helper class that exposes all GUIDs used across VS Package. 12 | /// 13 | internal sealed partial class PackageGuids 14 | { 15 | public const string guidVsPackageString = "3ec2fa73-1f0d-4e31-88c3-604c4e46ec14"; 16 | public static Guid guidVsPackage = new Guid(guidVsPackageString); 17 | 18 | public const string guidExportPackageCmdSetString = "e84b4658-2e40-46fc-90e5-f29db9b73b46"; 19 | public static Guid guidExportPackageCmdSet = new Guid(guidExportPackageCmdSetString); 20 | 21 | public const string guidExtensionMenuString = "d309f791-903f-11d0-9efc-00a0c911004f"; 22 | public static Guid guidExtensionMenu = new Guid(guidExtensionMenuString); 23 | } 24 | /// 25 | /// Helper class that encapsulates all CommandIDs uses across VS Package. 26 | /// 27 | internal sealed partial class PackageIds 28 | { 29 | public const int MyMenu = 0x0001; 30 | public const int MyMenuGroup = 0x1020; 31 | public const int ExportCmd = 0x0100; 32 | public const int ImportCmd = 0x0200; 33 | public const int ExportSolutionCmd = 0x0300; 34 | public const int guidExtensionMenuGroup = 0x6000; 35 | } 36 | } -------------------------------------------------------------------------------- /src/ExtensionManager.Vsix.VS2017/VsComandTable.vsct: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Import and Export Extensions 20 | 21 | 22 | 23 | 24 | 25 | 31 | 37 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/ExtensionManager.Vsix.VS2017/source.extension.cs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // 3 | // This file was generated by VSIX Synchronizer 4 | // 5 | // ------------------------------------------------------------------------------ 6 | namespace ExtensionManager 7 | { 8 | internal sealed partial class Vsix 9 | { 10 | public const string Id = "9cd6451c-c2fa-46fb-844a-a2535b824f1d"; 11 | public const string Name = "Extension Manager 2017"; 12 | public const string Description = @"Import/export extensions as well as associate extensions with individual solutions"; 13 | public const string Language = "en-US"; 14 | public const string Version = "9.9.9999"; 15 | public const string Author = "Loop8ack"; 16 | public const string Tags = "extension pack, vsix"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ExtensionManager.Vsix.VS2017/source.extension.vsixmanifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Extension Manager 2017 6 | Import/export extensions as well as associate extensions with individual solutions 7 | https://github.com/loop8ack/ExtensionPackTools 8 | Resources\LICENSE 9 | Resources\icon.png 10 | Resources\icon.png 11 | extension pack, vsix 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/ExtensionManager.Vsix.VS2019/VsComandTable.cs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // 3 | // This file was generated by VSIX Synchronizer 4 | // 5 | // ------------------------------------------------------------------------------ 6 | namespace ExtensionManager 7 | { 8 | using System; 9 | 10 | /// 11 | /// Helper class that exposes all GUIDs used across VS Package. 12 | /// 13 | internal sealed partial class PackageGuids 14 | { 15 | public const string guidVsPackageString = "3ec2fa73-1f0d-4e31-88c3-604c4e46ec14"; 16 | public static Guid guidVsPackage = new Guid(guidVsPackageString); 17 | 18 | public const string guidExportPackageCmdSetString = "e84b4658-2e40-46fc-90e5-f29db9b73b46"; 19 | public static Guid guidExportPackageCmdSet = new Guid(guidExportPackageCmdSetString); 20 | 21 | public const string guidExtensionMenuString = "d309f791-903f-11d0-9efc-00a0c911004f"; 22 | public static Guid guidExtensionMenu = new Guid(guidExtensionMenuString); 23 | } 24 | /// 25 | /// Helper class that encapsulates all CommandIDs uses across VS Package. 26 | /// 27 | internal sealed partial class PackageIds 28 | { 29 | public const int MyMenu = 0x0001; 30 | public const int MyMenuGroup = 0x1020; 31 | public const int ExportCmd = 0x0100; 32 | public const int ImportCmd = 0x0200; 33 | public const int ExportSolutionCmd = 0x0300; 34 | public const int guidExtensionMenuGroup = 0x6000; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ExtensionManager.Vsix.VS2019/VsComandTable.vsct: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Import and Export Extensions 20 | 21 | 22 | 23 | 24 | 25 | 31 | 37 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/ExtensionManager.Vsix.VS2019/source.extension.cs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // 3 | // This file was generated by VSIX Synchronizer 4 | // 5 | // ------------------------------------------------------------------------------ 6 | namespace ExtensionManager 7 | { 8 | internal sealed partial class Vsix 9 | { 10 | public const string Id = "e886a834-bf5c-4bc6-aabe-ffad88b4dc6e"; 11 | public const string Name = "Extension Manager 2019"; 12 | public const string Description = @"Import/export extensions as well as associate extensions with individual solutions"; 13 | public const string Language = "en-US"; 14 | public const string Version = "9.9.9999"; 15 | public const string Author = "Loop8ack"; 16 | public const string Tags = "extension pack, vsix"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ExtensionManager.Vsix.VS2019/source.extension.vsixmanifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Extension Manager 2019 6 | Import/export extensions as well as associate extensions with individual solutions 7 | https://github.com/loop8ack/ExtensionPackTools 8 | Resources\LICENSE 9 | Resources\icon.png 10 | Resources\icon.png 11 | extension pack, vsix 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/ExtensionManager.Vsix.VS2022/VsComandTable.cs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // 3 | // This file was generated by VSIX Synchronizer 4 | // 5 | // ------------------------------------------------------------------------------ 6 | namespace ExtensionManager 7 | { 8 | using System; 9 | 10 | /// 11 | /// Helper class that exposes all GUIDs used across VS Package. 12 | /// 13 | internal sealed partial class PackageGuids 14 | { 15 | public const string guidVsPackageString = "3ec2fa73-1f0d-4e31-88c3-604c4e46ec14"; 16 | public static Guid guidVsPackage = new Guid(guidVsPackageString); 17 | 18 | public const string guidExportPackageCmdSetString = "e84b4658-2e40-46fc-90e5-f29db9b73b46"; 19 | public static Guid guidExportPackageCmdSet = new Guid(guidExportPackageCmdSetString); 20 | 21 | public const string guidExtensionMenuString = "d309f791-903f-11d0-9efc-00a0c911004f"; 22 | public static Guid guidExtensionMenu = new Guid(guidExtensionMenuString); 23 | } 24 | /// 25 | /// Helper class that encapsulates all CommandIDs uses across VS Package. 26 | /// 27 | internal sealed partial class PackageIds 28 | { 29 | public const int MyMenu = 0x0001; 30 | public const int MyMenuGroup = 0x1020; 31 | public const int ExportCmd = 0x0100; 32 | public const int ImportCmd = 0x0200; 33 | public const int ExportSolutionCmd = 0x0300; 34 | public const int guidExtensionMenuGroup = 0x6000; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ExtensionManager.Vsix.VS2022/VsComandTable.vsct: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Import and Export Extensions 20 | 21 | 22 | 23 | 24 | 25 | 31 | 37 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/ExtensionManager.Vsix.VS2022/source.extension.cs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // 3 | // This file was generated by VSIX Synchronizer 4 | // 5 | // ------------------------------------------------------------------------------ 6 | namespace ExtensionManager 7 | { 8 | internal sealed partial class Vsix 9 | { 10 | public const string Id = "3d183c28-64c6-4efb-a201-50310d65e675"; 11 | public const string Name = "Extension Manager 2022"; 12 | public const string Description = @"Import/export extensions as well as associate extensions with individual solutions"; 13 | public const string Language = "en-US"; 14 | public const string Version = "9.9.9999"; 15 | public const string Author = "Loop8ack"; 16 | public const string Tags = "extension pack, vsix"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ExtensionManager.Vsix.VS2022/source.extension.vsixmanifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Extension Manager 2022 6 | Import/export extensions as well as associate extensions with individual solutions 7 | https://github.com/loop8ack/ExtensionPackTools 8 | Resources\LICENSE 9 | Resources\icon.png 10 | Resources\icon.png 11 | extension pack, vsix 12 | 13 | 14 | 15 | amd64 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/ExtensionManager.Vsix.md: -------------------------------------------------------------------------------- 1 | # ExtensionManager.Vsix 2 | 3 | These projects represent the actual extensions. 4 | They are separated from all other projects as they should contain minimal custom logic. 5 | Their sole purpose is to define the VSIX-specific contents and configurations. 6 | 7 | ## Projects 8 | 9 | | Project | Description | 10 | |---|---| 11 | | ExtensionManager.Vsix.2017 | The Visual Studio 2017 project | 12 | | ExtensionManager.Vsix.2019 | The Visual Studio 2019 project | 13 | | ExtensionManager.Vsix.2022 | The Visual Studio 2022 project | 14 | | ExtensionManager.Vsix.Shared | Contains the code that is not possible in SDK projects or is not worth outsourcing. This should only include the package implementation and the AssemblyInfo.cs file. | 15 | -------------------------------------------------------------------------------- /src/ExtensionManager.Vsix.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | $(GetVsixSourceItemsDependsOn);IncludeNuGetResolvedAssets 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/ExtensionManager.md: -------------------------------------------------------------------------------- 1 | # ExtensionManager 2 | 3 | These projects constitute the actual core of the solution. 4 | 5 | They are developed in the SDK style to simplify future development and better support newer Visual Studio and language features. 6 | 7 | ## Projects 8 | 9 | | Project | Description | 10 | |---|---| 11 | | ExtensionManager | The main project that contains the concrete functionalities of this extension | 12 | | ExtensionManager.Manifest | Contains code for reading and writing the manifest JSON file and supports versioning | 13 | | ExtensionManager.UI | Contains the entire user interface using the MVVM pattern | 14 | | ExtensionManager.Shared | Contains shared code. This should only include attribute classes from newer .NET versions to support newer C# features | 15 | -------------------------------------------------------------------------------- /src/ExtensionManager/ExtensionManager.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/ExtensionManager/FeatureExecutor.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.MessageBox; 2 | 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace ExtensionManager; 6 | 7 | internal sealed class FeatureExecutor : IFeatureExecutor 8 | { 9 | private readonly IServiceProvider _services; 10 | 11 | public FeatureExecutor(IServiceProvider services) 12 | { 13 | _services = services; 14 | } 15 | 16 | public async Task ExecuteAsync() 17 | where TFeature : class, IFeature 18 | { 19 | try 20 | { 21 | var feature = ActivatorUtilities.GetServiceOrCreateInstance(_services); 22 | 23 | await feature.ExecuteAsync().ConfigureAwait(false); 24 | } 25 | catch (Exception ex) 26 | { 27 | await _services 28 | .GetRequiredService() 29 | .ShowErrorAsync(ex.Message); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ExtensionManager/Features/Export/ExportFeature.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.Manifest; 2 | using ExtensionManager.UI.Worker; 3 | using ExtensionManager.VisualStudio.Extensions; 4 | 5 | namespace ExtensionManager.Features.Export; 6 | 7 | public sealed class ExportFeature : ExportFeatureBase 8 | { 9 | public ExportFeature(Args args) 10 | : base(args) 11 | { 12 | } 13 | 14 | protected override async Task GetFilePathAsync() 15 | => await DialogService.ShowSaveVsextFileDialogAsync(); 16 | 17 | protected override async Task ShowExportDialogAsync(IManifest manifest, IExportWorker worker, IReadOnlyCollection extensions) 18 | => await DialogService.ShowExportDialogAsync(worker, manifest, extensions); 19 | 20 | protected override async Task OnManifestWrittenAsync(string filePath) 21 | => await Documents.OpenAsync(filePath); 22 | } 23 | -------------------------------------------------------------------------------- /src/ExtensionManager/Features/Export/ExportFeatureBase.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.Manifest; 2 | using ExtensionManager.UI; 3 | using ExtensionManager.UI.Worker; 4 | using ExtensionManager.VisualStudio.Documents; 5 | using ExtensionManager.VisualStudio.Extensions; 6 | using ExtensionManager.VisualStudio.MessageBox; 7 | 8 | namespace ExtensionManager.Features.Export; 9 | 10 | public abstract class ExportFeatureBase : IFeature, IExportWorker 11 | { 12 | public sealed class Args 13 | { 14 | public IThisVsixInfo VsixInfo { get; } 15 | public IVSDocuments Documents { get; } 16 | public IVSMessageBox MessageBox { get; } 17 | public IVSExtensions Extensions { get; } 18 | public IDialogService DialogService { get; } 19 | public IManifestService ManifestService { get; } 20 | 21 | public Args(IThisVsixInfo vsixInfo, IVSDocuments documents, IVSMessageBox messageBox, IVSExtensions extensions, IDialogService dialogService, IManifestService manifestService) 22 | { 23 | VsixInfo = vsixInfo; 24 | Documents = documents; 25 | MessageBox = messageBox; 26 | Extensions = extensions; 27 | DialogService = dialogService; 28 | ManifestService = manifestService; 29 | } 30 | } 31 | 32 | private readonly Args _args; 33 | 34 | protected IThisVsixInfo VsixInfo => _args.VsixInfo; 35 | protected IVSDocuments Documents => _args.Documents; 36 | protected IVSMessageBox MessageBox => _args.MessageBox; 37 | protected IVSExtensions Extensions => _args.Extensions; 38 | protected IDialogService DialogService => _args.DialogService; 39 | protected IManifestService ManifestService => _args.ManifestService; 40 | 41 | protected ExportFeatureBase(Args args) 42 | { 43 | _args = args; 44 | } 45 | 46 | public async Task ExecuteAsync() 47 | { 48 | var manifest = ManifestService.CreateNew(); 49 | var installedExtensions = await Extensions.GetInstalledExtensionsAsync().ConfigureAwait(false); 50 | 51 | var installedExtensionsList = installedExtensions as List 52 | ?? installedExtensions.ToList(); 53 | 54 | installedExtensionsList.RemoveAll(vsix => vsix.Id == VsixInfo.Id); 55 | 56 | await ShowExportDialogAsync(manifest, this, installedExtensions); 57 | } 58 | 59 | async Task IExportWorker.ExportAsync(IManifest manifest, IProgress> progress, CancellationToken cancellationToken) 60 | { 61 | var filePath = await GetFilePathAsync().ConfigureAwait(false); 62 | 63 | if (filePath is null or { Length: 0 }) 64 | return; 65 | 66 | progress.Report(null, ExportStep.SaveManifest); 67 | await ManifestService.WriteAsync(filePath, manifest, cancellationToken).ConfigureAwait(false); 68 | 69 | progress.Report(null, ExportStep.Finish); 70 | await OnManifestWrittenAsync(filePath); 71 | } 72 | 73 | protected abstract Task GetFilePathAsync(); 74 | protected abstract Task ShowExportDialogAsync(IManifest manifest, IExportWorker worker, IReadOnlyCollection installedExtensions); 75 | protected abstract Task OnManifestWrittenAsync(string filePath); 76 | } 77 | -------------------------------------------------------------------------------- /src/ExtensionManager/Features/Export/ExportSolutionFeature.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.Manifest; 2 | using ExtensionManager.UI.Worker; 3 | using ExtensionManager.VisualStudio.Extensions; 4 | using ExtensionManager.VisualStudio.Solution; 5 | 6 | namespace ExtensionManager.Features.Export; 7 | 8 | public sealed class ExportSolutionFeature : ExportFeatureBase 9 | { 10 | private readonly IVSSolutions _solutions; 11 | 12 | public ExportSolutionFeature(Args args, IVSSolutions solutions) 13 | : base(args) 14 | { 15 | _solutions = solutions; 16 | } 17 | 18 | protected override async Task GetFilePathAsync() 19 | => await _solutions.GetCurrentSolutionExtensionsManifestFilePathAsync(MessageBox); 20 | 21 | protected override async Task ShowExportDialogAsync(IManifest manifest, IExportWorker worker, IReadOnlyCollection installedExtensions) 22 | => await DialogService.ShowExportForSolutionDialogAsync(worker, manifest, installedExtensions); 23 | 24 | protected override async Task OnManifestWrittenAsync(string filePath) 25 | { 26 | const string folderName = "Solution Items"; 27 | 28 | var solution = await _solutions.GetCurrentOrThrowAsync(); 29 | var solutionChildren = await solution.GetChildrenAsync(); 30 | 31 | var folder = solutionChildren.SingleOrDefault(x => x.Name == folderName) as IVSSolutionFolder 32 | ?? await solution.AddSolutionFolderAsync(folderName); 33 | 34 | if (folder is null) 35 | throw new InvalidOperationException("Could not add solution folder"); 36 | 37 | await folder.AddExistingFilesAsync(filePath); 38 | await Documents.OpenAsync(filePath); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ExtensionManager/Features/Install/InstallFeature.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.Manifest; 2 | using ExtensionManager.UI; 3 | using ExtensionManager.UI.Worker; 4 | 5 | namespace ExtensionManager.Features.Install; 6 | 7 | public sealed class InstallFeature : InstallFeatureBase 8 | { 9 | public InstallFeature(Args args) 10 | : base(args) 11 | { 12 | } 13 | 14 | protected override async Task GetFilePathAsync() 15 | => await DialogService.ShowOpenVsextFileDialogAsync(); 16 | 17 | protected override async Task ShowInstallDialogAsync(IManifest manifest, IInstallWorker worker, IReadOnlyCollection extensions) 18 | => await DialogService.ShowInstallDialogAsync(worker, manifest, extensions); 19 | } 20 | -------------------------------------------------------------------------------- /src/ExtensionManager/Features/Install/InstallFeatureBase.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.Installation; 2 | using ExtensionManager.Manifest; 3 | using ExtensionManager.UI; 4 | using ExtensionManager.UI.Worker; 5 | using ExtensionManager.Utils; 6 | using ExtensionManager.VisualStudio.Extensions; 7 | using ExtensionManager.VisualStudio.MessageBox; 8 | 9 | namespace ExtensionManager.Features.Install; 10 | 11 | public abstract class InstallFeatureBase : IFeature, IInstallWorker 12 | { 13 | public sealed class Args 14 | { 15 | public IVSExtensions Extensions { get; } 16 | public IVSMessageBox MessageBox { get; } 17 | public IDialogService DialogService { get; } 18 | public IExtensionInstaller Installer { get; } 19 | public IManifestService ManifestService { get; } 20 | 21 | public Args(IVSExtensions extensions, IVSMessageBox messageBox, IDialogService dialogService, IExtensionInstaller installer, IManifestService manifestService) 22 | { 23 | Extensions = extensions; 24 | MessageBox = messageBox; 25 | DialogService = dialogService; 26 | Installer = installer; 27 | ManifestService = manifestService; 28 | } 29 | } 30 | 31 | private readonly Args _args; 32 | 33 | protected IVSExtensions Extensions => _args.Extensions; 34 | protected IVSMessageBox MessageBox => _args.MessageBox; 35 | protected IDialogService DialogService => _args.DialogService; 36 | protected IExtensionInstaller Installer => _args.Installer; 37 | protected IManifestService ManifestService => _args.ManifestService; 38 | 39 | protected InstallFeatureBase(Args args) 40 | { 41 | _args = args; 42 | } 43 | 44 | public async Task ExecuteAsync() 45 | { 46 | var filePath = await GetFilePathAsync().ConfigureAwait(false); 47 | 48 | if (filePath is null or { Length: 0 }) 49 | return; 50 | 51 | if (!File.Exists(filePath)) 52 | return; 53 | 54 | var manifest = await ManifestService.ReadAsync(filePath).ConfigureAwait(false); 55 | var extensionsToInstall = await CreateExtensionsToInstallListAsync(manifest.Extensions).ConfigureAwait(false); 56 | 57 | await ShowInstallDialogAsync(manifest, this, extensionsToInstall.ToList()); 58 | } 59 | 60 | protected virtual async Task> CreateExtensionsToInstallListAsync(IEnumerable toInstall) 61 | { 62 | var installed = await Extensions.GetInstalledExtensionsAsync().ConfigureAwait(false); 63 | var gallery = await Extensions.GetGalleryExtensionsAsync(toInstall.Select(x => x.Id)).ConfigureAwait(false); 64 | 65 | var statuses = new Dictionary(); 66 | 67 | foreach (var extension in toInstall) 68 | statuses[extension.Id] = VSExtensionStatus.NotSupported; 69 | 70 | foreach (var extension in gallery) 71 | statuses[extension.Id] = VSExtensionStatus.NotInstalled; 72 | 73 | foreach (var extension in installed.Intersect(toInstall, ExtensionEqualityComparer.Instance)) 74 | statuses[extension.Id] = VSExtensionStatus.Installed; 75 | 76 | return toInstall 77 | .Distinct(ExtensionEqualityComparer.Instance) 78 | .Select(x => new VSExtensionToInstall(x, statuses[x.Id])); 79 | } 80 | 81 | async Task IInstallWorker.InstallAsync(IManifest manifest, IReadOnlyCollection extensions, bool systemWide, IProgress> progress, CancellationToken cancellationToken) 82 | { 83 | if (extensions.Count > 0) 84 | await Installer.InstallAsync(extensions, systemWide, progress, cancellationToken); 85 | } 86 | 87 | protected abstract Task GetFilePathAsync(); 88 | protected abstract Task ShowInstallDialogAsync(IManifest manifest, IInstallWorker worker, IReadOnlyCollection extensions); 89 | } 90 | -------------------------------------------------------------------------------- /src/ExtensionManager/Features/Install/InstallForSolutionFeature.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.Manifest; 2 | using ExtensionManager.UI; 3 | using ExtensionManager.UI.Worker; 4 | using ExtensionManager.VisualStudio.Extensions; 5 | using ExtensionManager.VisualStudio.Solution; 6 | 7 | namespace ExtensionManager.Features.Install; 8 | 9 | public sealed class InstallForSolutionFeature : InstallFeatureBase 10 | { 11 | private readonly IVSSolutions _solutions; 12 | 13 | public InstallForSolutionFeature(Args args, IVSSolutions solutions) 14 | : base(args) 15 | { 16 | _solutions = solutions; 17 | } 18 | 19 | protected override async Task GetFilePathAsync() 20 | => await _solutions.GetCurrentSolutionExtensionsManifestFilePathAsync(MessageBox); 21 | 22 | protected override async Task> CreateExtensionsToInstallListAsync(IEnumerable toInstall) 23 | { 24 | var extensions = await base.CreateExtensionsToInstallListAsync(toInstall); 25 | 26 | return extensions 27 | .Where(x => x.Status != VSExtensionStatus.Installed); 28 | } 29 | 30 | protected override async Task ShowInstallDialogAsync(IManifest manifest, IInstallWorker worker, IReadOnlyCollection extensions) 31 | { 32 | if (extensions.Count == 0) 33 | return; 34 | 35 | await DialogService.ShowInstallForSolutionDialogAsync(worker, manifest, extensions); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ExtensionManager/Features/VisualStudioExtensions.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.Extensions; 2 | using ExtensionManager.VisualStudio.MessageBox; 3 | using ExtensionManager.VisualStudio.Solution; 4 | 5 | namespace ExtensionManager.Features; 6 | 7 | internal static class VisualStudioExtensions 8 | { 9 | public static async Task> GetInstalledExtensionsAsync(this IVSExtensions extensions) 10 | { 11 | return (await extensions.GetInstalledExtensionsAsync().ConfigureAwait(false)) 12 | .OrderBy(e => e.Name) 13 | .ToList(); 14 | } 15 | 16 | public static async Task GetCurrentSolutionExtensionsManifestFilePathAsync(this IVSSolutions solutions, IVSMessageBox messageBox) 17 | { 18 | var solution = await solutions.GetCurrentOrThrowAsync(); 19 | 20 | if (solution.FullPath is null or { Length: 0 }) 21 | { 22 | await messageBox.ShowErrorAsync("The solution must be saved in order to manage solution extensions.").ConfigureAwait(false); 23 | 24 | return null; 25 | } 26 | 27 | return Path.ChangeExtension(solution.FullPath, ".vsext"); 28 | } 29 | 30 | public static async Task GetCurrentOrThrowAsync(this IVSSolutions solutions) 31 | { 32 | return await solutions.GetCurrentSolutionAsync() 33 | ?? throw new InvalidOperationException("No solution is loaded"); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ExtensionManager/IFeature.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager; 2 | 3 | public interface IFeature 4 | { 5 | Task ExecuteAsync(); 6 | } 7 | -------------------------------------------------------------------------------- /src/ExtensionManager/IFeatureExecutor.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager; 2 | public interface IFeatureExecutor 3 | { 4 | Task ExecuteAsync() where TFeature : class, IFeature; 5 | } -------------------------------------------------------------------------------- /src/ExtensionManager/IThisVsixInfo.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager; 2 | 3 | public interface IThisVsixInfo 4 | { 5 | string Id { get; } 6 | string Name { get; } 7 | string Description { get; } 8 | string Language { get; } 9 | string Version { get; } 10 | string Author { get; } 11 | string Tags { get; } 12 | } 13 | -------------------------------------------------------------------------------- /src/ExtensionManager/Installation/DownloadProgres.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Diagnostics; 3 | 4 | using ExtensionManager.UI.Worker; 5 | using ExtensionManager.VisualStudio.StatusBar; 6 | 7 | namespace ExtensionManager.Installation; 8 | 9 | internal sealed class DownloadProgres : IProgress 10 | { 11 | private readonly IProgress> _uiProgress; 12 | private readonly IVSStatusBar _statusBar; 13 | private readonly int _initialCount; 14 | private int _remainingCount; 15 | private int _failedCount; 16 | 17 | public DownloadProgres(IProgress> uiProgress, IVSStatusBar statusBar, int initialCount) 18 | { 19 | _uiProgress = uiProgress; 20 | _statusBar = statusBar; 21 | _initialCount = initialCount; 22 | _remainingCount = initialCount; 23 | _failedCount = 0; 24 | } 25 | 26 | public void Report(DownloadResult value) 27 | { 28 | int remainingCount, failedCount; 29 | 30 | switch (value) 31 | { 32 | case DownloadResult.Success: 33 | remainingCount = Interlocked.Decrement(ref _remainingCount); 34 | failedCount = _failedCount; 35 | break; 36 | 37 | case DownloadResult.Failure: 38 | remainingCount = _remainingCount; 39 | failedCount = Interlocked.Increment(ref _failedCount); 40 | break; 41 | 42 | default: 43 | throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(DownloadResult)); 44 | } 45 | 46 | var text = failedCount > 0 47 | ? $"Downloading {remainingCount} extensions, {failedCount} failed ..." 48 | : $"Downloading {remainingCount} extensions ..."; 49 | 50 | var currentCount = _initialCount - remainingCount; 51 | var percentage = currentCount / (float)_initialCount; 52 | 53 | Debug.WriteLine($"====== {currentCount} - {_initialCount}"); 54 | 55 | _uiProgress.Report(percentage, InstallStep.DownloadVsix); 56 | _ = _statusBar.ShowProgressAsync(text, currentCount, _initialCount); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/ExtensionManager/Installation/DownloadResult.cs: -------------------------------------------------------------------------------- 1 | namespace ExtensionManager.Installation; 2 | 3 | internal enum DownloadResult : byte 4 | { 5 | Success, 6 | Failure, 7 | } 8 | -------------------------------------------------------------------------------- /src/ExtensionManager/Installation/ExtensionDownloader.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | 3 | using ExtensionManager.VisualStudio.Extensions; 4 | 5 | using Task = System.Threading.Tasks.Task; 6 | 7 | namespace ExtensionManager.Installation; 8 | 9 | internal sealed class ExtensionDownloader 10 | { 11 | private readonly HttpMessageHandler _httpMessageHandler; 12 | private readonly IProgress _progress; 13 | private readonly CancellationToken _cancellationToken; 14 | 15 | public Uri DownloadUri { get; } 16 | public string TargetFilePath { get; } 17 | public IVSExtension Extension { get; } 18 | 19 | public Task DownloadTask { get; private set; } 20 | public Exception? DownloadException { get; private set; } 21 | 22 | public ExtensionDownloader(HttpMessageHandler httpMessageHandler, IProgress progress, Uri downloadUri, string targetFilePath, IVSExtension extension, CancellationToken cancellationToken) 23 | { 24 | _httpMessageHandler = httpMessageHandler; 25 | _progress = progress; 26 | _cancellationToken = cancellationToken; 27 | 28 | DownloadUri = downloadUri; 29 | TargetFilePath = targetFilePath; 30 | Extension = extension; 31 | 32 | DownloadTask = Task.FromException(new InvalidOperationException("The download has not yet been started")); 33 | } 34 | 35 | public void BeginDownload() 36 | { 37 | DownloadException = null; 38 | DownloadTask = DownloadAsync(); 39 | } 40 | 41 | private async Task DownloadAsync() 42 | { 43 | _cancellationToken.ThrowIfCancellationRequested(); 44 | 45 | await Task.Delay(1000, _cancellationToken); 46 | 47 | try 48 | { 49 | using (var client = new HttpClient(_httpMessageHandler, disposeHandler: false)) 50 | using (var response = await client.GetAsync(DownloadUri, _cancellationToken).ConfigureAwait(false)) 51 | using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) 52 | using (var targetStream = new FileStream(TargetFilePath, FileMode.Create, FileAccess.Write)) 53 | await responseStream.CopyToAsync(targetStream, 81920, _cancellationToken).ConfigureAwait(false); 54 | 55 | _progress.Report(DownloadResult.Success); 56 | } 57 | catch (Exception ex) 58 | { 59 | _progress.Report(DownloadResult.Failure); 60 | 61 | DownloadException = ex; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/ExtensionManager/Installation/IExtensionInstaller.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.UI.Worker; 2 | using ExtensionManager.VisualStudio.Extensions; 3 | 4 | namespace ExtensionManager.Installation; 5 | 6 | public interface IExtensionInstaller 7 | { 8 | Task InstallAsync(IReadOnlyCollection extensions, bool installSystemWide, IProgress> uiProgress, CancellationToken cancellationToken); 9 | } 10 | -------------------------------------------------------------------------------- /src/ExtensionManager/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.Features.Export; 2 | using ExtensionManager.Features.Install; 3 | using ExtensionManager.Installation; 4 | using ExtensionManager.Manifest; 5 | using ExtensionManager.UI; 6 | 7 | using Microsoft.Extensions.DependencyInjection; 8 | 9 | namespace ExtensionManager; 10 | 11 | public static class ServiceCollectionExtensions 12 | { 13 | public static IServiceCollection ConfigureExtensionManager(this IServiceCollection services, IThisVsixInfo thisVsixInfo) 14 | { 15 | return services 16 | .AddDialogService() 17 | .AddManifestService() 18 | .AddSingleton(thisVsixInfo) 19 | .AddTransient() 20 | .AddTransient() 21 | .AddTransient() 22 | .AddTransient(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ExtensionManager/Utils/ExtensionEqualityComparer.cs: -------------------------------------------------------------------------------- 1 | using ExtensionManager.VisualStudio.Extensions; 2 | 3 | namespace ExtensionManager.Utils; 4 | 5 | internal sealed class ExtensionEqualityComparer : IEqualityComparer 6 | { 7 | public static ExtensionEqualityComparer Instance { get; } = new ExtensionEqualityComparer(); 8 | 9 | public bool Equals(IVSExtension x, IVSExtension y) 10 | => x.Id == y.Id; 11 | 12 | public int GetHashCode(IVSExtension obj) 13 | => obj.Id.GetHashCode(); 14 | } 15 | --------------------------------------------------------------------------------