├── .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://github.com/loop8ack/ExtensionPackTools/actions/workflows/Build.yml)
5 | [](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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
13 |
14 |
15 |
16 |
22 |
23 |
24 |
25 |
31 |
37 |
47 |
48 |
49 |
50 |
51 |
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 |
13 |
14 |
15 |
16 |
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 |
13 |
14 |
15 |
16 |
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 |
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 |
--------------------------------------------------------------------------------