diff --git a/CodeConv/CodeConv.csproj b/CodeConv/CodeConv.csproj index 6a1f9230..7a82d4d6 100644 --- a/CodeConv/CodeConv.csproj +++ b/CodeConv/CodeConv.csproj @@ -31,6 +31,15 @@ + + diff --git a/CodeConverter/CSharp/HandledEventsAnalyzer.cs b/CodeConverter/CSharp/HandledEventsAnalyzer.cs index f15efb17..4c9c1e38 100644 --- a/CodeConverter/CSharp/HandledEventsAnalyzer.cs +++ b/CodeConverter/CSharp/HandledEventsAnalyzer.cs @@ -37,8 +37,10 @@ private async Task AnalyzeAsync() #pragma warning restore RS1024 // Compare symbols correctly - var writtenWithEventsProperties = await ancestorPropsMembersByName.Values.OfType().ToAsyncEnumerable() - .ToDictionaryAsync(async (p, _) => p.Name, async (p, cancellationToken) => (p, await IsNeverWrittenOrOverriddenAsync(p, cancellationToken))); + var writtenWithEventsProperties = new Dictionary(); + foreach (var prop in ancestorPropsMembersByName.Values.OfType()) { + writtenWithEventsProperties[prop.Name] = (prop, await IsNeverWrittenOrOverriddenAsync(prop)); + } var eventContainerToMethods = _type.GetMembers().OfType() .SelectMany(HandledEvents) diff --git a/CodeConverter/CSharp/ProjectMergedDeclarationExtensions.cs b/CodeConverter/CSharp/ProjectMergedDeclarationExtensions.cs index ac42c210..deab051c 100644 --- a/CodeConverter/CSharp/ProjectMergedDeclarationExtensions.cs +++ b/CodeConverter/CSharp/ProjectMergedDeclarationExtensions.cs @@ -63,7 +63,7 @@ public static async Task WithRenamedMergedMyNamespaceAsync(this Project var projectDir = Path.Combine(vbProject.GetDirectoryPath(), "My Project"); var compilation = await vbProject.GetCompilationAsync(cancellationToken); - var embeddedSourceTexts = await GetAllEmbeddedSourceTextAsync(compilation).Select((r, i) => (Text: r, Suffix: $".Static.{i+1}")).ToArrayAsync(cancellationToken); + var embeddedSourceTexts = await GetAllEmbeddedSourceTextAsync(compilation).SelectSafe((r, i) => (Text: r, Suffix: $".Static.{i+1}")).ToArraySafeAsync(cancellationToken); var generatedSourceTexts = (Text: await GetDynamicallyGeneratedSourceTextAsync(compilation), Suffix: ".Dynamic").Yield(); foreach (var (text, suffix) in embeddedSourceTexts.Concat(generatedSourceTexts)) { diff --git a/CodeConverter/CodeConverter.csproj b/CodeConverter/CodeConverter.csproj index 3f2b3a80..43cd4408 100644 --- a/CodeConverter/CodeConverter.csproj +++ b/CodeConverter/CodeConverter.csproj @@ -54,9 +54,6 @@ - - - all diff --git a/CodeConverter/Common/AsyncEnumerableTaskExtensions.cs b/CodeConverter/Common/AsyncEnumerableTaskExtensions.cs index 58d3a367..fb7d786c 100644 --- a/CodeConverter/Common/AsyncEnumerableTaskExtensions.cs +++ b/CodeConverter/Common/AsyncEnumerableTaskExtensions.cs @@ -92,4 +92,68 @@ public static async Task SelectAsync(this IEnumerable< return partitionResults.ToArray(); } + + /// + /// Hand-rolled to avoid depending on System.Linq.AsyncEnumerable, which only ships as a 10.x + /// package and so transitively forces Microsoft.Bcl.AsyncInterfaces 10.0.0.0 into the Vsix output — + /// a version Visual Studio 17.x cannot bind to. See VsixAssemblyCompatibilityTests. + /// + /// + /// Deliberately not named ToArrayAsync so it does not clash with + /// .ToArrayAsync on .NET 10+ when this assembly is + /// referenced by a project whose target framework provides the BCL version. + /// + public static async Task ToArraySafeAsync(this IAsyncEnumerable source, + CancellationToken cancellationToken = default) + { + var list = new List(); + await foreach (var item in source.WithCancellation(cancellationToken)) { + list.Add(item); + } + return list.ToArray(); + } + + /// + /// Adapts a synchronous sequence to . Hand-rolled for the same + /// reason as , and likewise renamed to avoid clashing with + /// .ToAsyncEnumerable on .NET 10+. + /// +#pragma warning disable 1998 // async method without await; required for the iterator to compile to IAsyncEnumerable. +#pragma warning disable VSTHRD200 // The method returns IAsyncEnumerable, not a Task; "Async" suffix would be misleading. + public static async IAsyncEnumerable AsAsyncEnumerable(this IEnumerable source) + { + foreach (var item in source) { + yield return item; + } + } +#pragma warning restore 1998 + + /// + /// Lazy projection over an . Hand-rolled for the same reason + /// as , and renamed to avoid clashing with + /// .Select on .NET 10+. + /// + public static async IAsyncEnumerable SelectSafe(this IAsyncEnumerable source, + Func selector, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await foreach (var item in source.WithCancellation(cancellationToken)) { + yield return selector(item); + } + } + + /// + /// Lazy projection (with index) over an . Hand-rolled for the same + /// reason as . + /// + public static async IAsyncEnumerable SelectSafe(this IAsyncEnumerable source, + Func selector, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var index = 0; + await foreach (var item in source.WithCancellation(cancellationToken)) { + yield return selector(item, index++); + } + } +#pragma warning restore VSTHRD200 } \ No newline at end of file diff --git a/CodeConverter/Common/ProjectConversion.cs b/CodeConverter/Common/ProjectConversion.cs index 287c386e..bfd9cad6 100644 --- a/CodeConverter/Common/ProjectConversion.cs +++ b/CodeConverter/Common/ProjectConversion.cs @@ -50,7 +50,7 @@ private ProjectConversion(IProjectContentsConverter projectContentsConverter, IE if (conversionOptions.SelectedTextSpan is { Length: > 0 } span) { document = await WithAnnotatedSelectionAsync(document, span); } - var conversionResults = await ConvertDocumentsAsync(new[] {document}, conversionOptions, progress, cancellationToken).ToArrayAsync(cancellationToken); + var conversionResults = await ConvertDocumentsAsync(new[] {document}, conversionOptions, progress, cancellationToken).ToArraySafeAsync(cancellationToken); var codeResult = conversionResults.First(r => r.SourcePathOrNull == document.FilePath); codeResult.Exceptions = conversionResults.SelectMany(x => x.Exceptions).ToArray(); return codeResult; @@ -183,7 +183,7 @@ private async IAsyncEnumerable ConvertAsync(IProgress FirstPassLoggedAsync(d, phaseProgress), Env.MaxDop, _cancellationToken); - var (proj1, docs1) = await _projectContentsConverter.GetConvertedProjectAsync(await firstPassResults.ToArrayAsync(_cancellationToken)); + var (proj1, docs1) = await _projectContentsConverter.GetConvertedProjectAsync(await firstPassResults.ToArraySafeAsync(_cancellationToken)); var warnings = await GetProjectWarningsAsync(_projectContentsConverter.SourceProject, proj1); if (!string.IsNullOrWhiteSpace(warnings)) { @@ -193,7 +193,7 @@ private async IAsyncEnumerable ConvertAsync(IProgress SecondPassLoggedAsync(d, phaseProgress), Env.MaxDop, _cancellationToken); - await foreach (var result in secondPassResults.Select(CreateConversionResult).WithCancellation(_cancellationToken)) { + await foreach (var result in secondPassResults.SelectSafe(CreateConversionResult).WithCancellation(_cancellationToken)) { yield return result; } await foreach (var result in _projectContentsConverter.GetAdditionalConversionResultsAsync(_additionalDocumentsToConvert, _cancellationToken)) { diff --git a/CodeConverter/Common/SolutionConverter.cs b/CodeConverter/Common/SolutionConverter.cs index 2d5b8bda..19d7316f 100644 --- a/CodeConverter/Common/SolutionConverter.cs +++ b/CodeConverter/Common/SolutionConverter.cs @@ -71,17 +71,20 @@ private SolutionConverter(string solutionFilePath, public async IAsyncEnumerable ConvertAsync() { var projectsToUpdateReferencesOnly = _projectsToConvert.First().Solution.Projects.Except(_projectsToConvert); - var solutionResult = string.IsNullOrWhiteSpace(_sourceSolutionContents) ? Enumerable.Empty() : ConvertSolutionFile().Yield(); - var convertedProjects = await ConvertProjectsAsync(); - var projectsAndSolutionResults = UpdateProjectReferences(projectsToUpdateReferencesOnly).Concat(solutionResult).ToAsyncEnumerable(); - await foreach (var p in convertedProjects.Concat(projectsAndSolutionResults)) { - yield return p; + + foreach (var project in _projectsToConvert) { + await foreach (var result in ConvertProjectAsync(project).WithCancellation(_cancellationToken)) { + yield return result; + } } - } - private async Task> ConvertProjectsAsync() - { - return _projectsToConvert.ToAsyncEnumerable().SelectMany(ConvertProjectAsync); + foreach (var result in UpdateProjectReferences(projectsToUpdateReferencesOnly)) { + yield return result; + } + + if (!string.IsNullOrWhiteSpace(_sourceSolutionContents)) { + yield return ConvertSolutionFile(); + } } private IAsyncEnumerable ConvertProjectAsync(Project project) diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index f25cb7b9..321c9273 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -41,4 +41,13 @@ + + + + diff --git a/Tests/Vsix/VsixAssemblyCompatibilityTests.cs b/Tests/Vsix/VsixAssemblyCompatibilityTests.cs new file mode 100644 index 00000000..2b2f4646 --- /dev/null +++ b/Tests/Vsix/VsixAssemblyCompatibilityTests.cs @@ -0,0 +1,211 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using Xunit; +using Xunit.Abstractions; + +namespace ICSharpCode.CodeConverter.Tests.Vsix; + +/// +/// When the Vsix loads into Visual Studio, every assembly it references must be resolvable +/// by the running devenv.exe. For a subset of BCL polyfill assemblies (Microsoft.Bcl.AsyncInterfaces +/// and friends) VS ships a specific assembly version and defines a binding redirect in devenv.exe.config +/// covering 0.0.0.0-<shippedVersion>. Any reference whose assembly version exceeds that range +/// is *not* redirected, so the CLR probes the extension folder instead. That path succeeds only if we +/// happen to ship exactly the requested version, which in turn leads to two different copies of +/// (say) System.IAsyncDisposable loaded side by side and silent +/// / failures at runtime. +/// +/// This test reproduces that class of issue without needing a real Visual Studio by statically +/// walking the Vsix output directory. For each supported VS baseline it asserts that every +/// referenced version of a known VS-owned polyfill can be satisfied by the version VS ships plus +/// the binding redirect it declares. The test fails loudly when the Vsix pulls in a newer BCL +/// polyfill than the oldest supported VS. +/// +public class VsixAssemblyCompatibilityTests +{ + private readonly ITestOutputHelper _output; + + public VsixAssemblyCompatibilityTests(ITestOutputHelper output) + { + _output = output; + } + + /// + /// The set of BCL polyfill assemblies that VS ships and binding-redirects itself. We must + /// not reference a newer version than the oldest supported VS ships, otherwise VS's + /// binding redirect can't cover our reference and we end up with duplicate type identities. + /// + /// + /// Versions taken from the BCL polyfill shipped by the corresponding Microsoft.VisualStudio.Threading + /// package (which is what the devenv.exe.config binding redirect tracks): + /// VS 17.14 (the oldest VS2022 we claim to support): Microsoft.VisualStudio.Threading 17.14 → Microsoft.Bcl.AsyncInterfaces 9.0.0.0 + /// VS 18.x (VS2026 preview): Microsoft.VisualStudio.Threading 18.x → Microsoft.Bcl.AsyncInterfaces 10.0.0.0 + /// The oldest supported VS sets the ceiling on what we can reference. + /// + private static readonly IReadOnlyDictionary OldestSupportedVsPolyfillVersions = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Microsoft.Bcl.AsyncInterfaces"] = new Version(9, 0, 0, 0), + }; + + [Fact] + public void VsixDoesNotReferenceNewerBclPolyfillsThanOldestSupportedVs() + { + var vsixOutput = FindVsixOutputDirectory(); + Assert.True(Directory.Exists(vsixOutput), + $"Expected Vsix output at '{vsixOutput}'. Build the Vsix project first (msbuild Vsix\\Vsix.csproj)."); + + var references = CollectReferencesByAssemblyName(vsixOutput); + var files = CollectFileVersionsByAssemblyName(vsixOutput); + + var failures = new List(); + foreach (var (polyfillName, maxSupportedVersion) in OldestSupportedVsPolyfillVersions) { + if (!references.TryGetValue(polyfillName, out var refs) || refs.Count == 0) { + continue; + } + + var sorted = refs.OrderBy(r => r.RequestedVersion).ToList(); + _output.WriteLine($"{polyfillName}: oldest supported VS ships {maxSupportedVersion}"); + foreach (var r in sorted) { + _output.WriteLine($" {r.ReferrerFileName} -> {polyfillName} {r.RequestedVersion}"); + } + + var maxRequested = sorted.Max(r => r.RequestedVersion); + if (maxRequested > maxSupportedVersion) { + var offenders = sorted.Where(r => r.RequestedVersion > maxSupportedVersion) + .Select(r => $"{r.ReferrerFileName} -> {r.RequestedVersion}"); + failures.Add( + $"{polyfillName}: oldest supported VS ships {maxSupportedVersion}, but {string.Join(", ", offenders)} " + + "reference a newer version that VS's devenv.exe.config binding redirect cannot unify. " + + "Either downgrade the referencing package so the compile-time reference is <= " + + $"{maxSupportedVersion} or add a [ProvideBindingRedirection] attribute to the CodeConverterPackage."); + } + + if (files.TryGetValue(polyfillName, out var onDisk) && onDisk.Version > maxSupportedVersion) { + failures.Add( + $"{polyfillName}: {Path.GetFileName(onDisk.Path)} in the Vsix output is {onDisk.Version} " + + $"but the oldest supported VS only ships {maxSupportedVersion}. " + + "When both copies load into devenv.exe the CLR will create duplicate type identities."); + } + } + + Assert.True(failures.Count == 0, string.Join(Environment.NewLine, failures)); + } + + [Fact] + public void VsixReferencesToSameAssemblyAgreeOrAreCoveredByShippedFile() + { + var vsixOutput = FindVsixOutputDirectory(); + if (!Directory.Exists(vsixOutput)) { + return; + } + + var references = CollectReferencesByAssemblyName(vsixOutput); + var files = CollectFileVersionsByAssemblyName(vsixOutput); + + var splits = references + .Where(kv => kv.Value.Select(r => r.RequestedVersion).Distinct().Count() > 1) + .Where(kv => OldestSupportedVsPolyfillVersions.ContainsKey(kv.Key)) + .ToList(); + + foreach (var kv in splits) { + var distinct = kv.Value.Select(r => r.RequestedVersion).Distinct().OrderBy(v => v).ToList(); + var hasFile = files.TryGetValue(kv.Key, out var fileEntry); + _output.WriteLine($"Split-version reference to {kv.Key}:"); + foreach (var v in distinct) { + var referrers = kv.Value.Where(r => r.RequestedVersion == v).Select(r => r.ReferrerFileName); + _output.WriteLine($" v{v}: {string.Join(", ", referrers)}"); + } + _output.WriteLine(hasFile + ? $" File on disk: {Path.GetFileName(fileEntry.Path)} v{fileEntry.Version}" + : " File on disk: (not shipped; relying on VS-installed copy)"); + } + } + + private static string FindVsixOutputDirectory() + { + // Tests/bin// -> Vsix/bin/ + var testAssembly = typeof(VsixAssemblyCompatibilityTests).Assembly.Location; + var dir = new DirectoryInfo(Path.GetDirectoryName(testAssembly)!); + // Walk up looking for the repo root (containing Vsix folder) + while (dir != null && !Directory.Exists(Path.Combine(dir.FullName, "Vsix", "bin"))) { + dir = dir.Parent; + } + if (dir == null) { + return Path.Combine(AppContext.BaseDirectory, "Vsix", "bin", "Release"); + } + var vsixBin = Path.Combine(dir.FullName, "Vsix", "bin"); + // Prefer the same configuration the tests were built with + var configDirs = Directory.EnumerateDirectories(vsixBin).ToList(); + // Prefer Release if it exists, otherwise any configuration. + var release = configDirs.FirstOrDefault(d => string.Equals(Path.GetFileName(d), "Release", StringComparison.OrdinalIgnoreCase)); + return release ?? configDirs.FirstOrDefault() ?? Path.Combine(vsixBin, "Release"); + } + + private record ReferenceEntry(string ReferrerFileName, Version RequestedVersion); + + private static Dictionary> CollectReferencesByAssemblyName(string directory) + { + var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var dll in Directory.EnumerateFiles(directory, "*.dll")) { + if (!TryOpenMetadata(dll, out var md)) { + continue; + } + using (md) { + var reader = md!.GetMetadataReader(); + if (!reader.IsAssembly) continue; + foreach (var handle in reader.AssemblyReferences) { + var reference = reader.GetAssemblyReference(handle); + var name = reader.GetString(reference.Name); + if (!result.TryGetValue(name, out var list)) { + list = new List(); + result[name] = list; + } + list.Add(new ReferenceEntry(Path.GetFileName(dll), reference.Version)); + } + } + } + return result; + } + + private static Dictionary CollectFileVersionsByAssemblyName(string directory) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var dll in Directory.EnumerateFiles(directory, "*.dll")) { + if (!TryOpenMetadata(dll, out var md)) { + continue; + } + using (md) { + var reader = md!.GetMetadataReader(); + if (!reader.IsAssembly) continue; + var def = reader.GetAssemblyDefinition(); + result[reader.GetString(def.Name)] = (def.Version, dll); + } + } + return result; + } + + private static bool TryOpenMetadata(string path, out PEReader? peReader) + { + peReader = null; + try { + var stream = File.OpenRead(path); + peReader = new PEReader(stream); + if (!peReader.HasMetadata) { + peReader.Dispose(); + peReader = null; + return false; + } + return true; + } catch { + peReader?.Dispose(); + peReader = null; + return false; + } + } +} diff --git a/Vsix/CodeConversion.cs b/Vsix/CodeConversion.cs index 2d70dd3d..0720a15c 100644 --- a/Vsix/CodeConversion.cs +++ b/Vsix/CodeConversion.cs @@ -91,7 +91,7 @@ await _joinableTaskFactory.RunAsync(async () => { await EnsureBuiltAsync(containingProject is null ? Array.Empty() : new[]{containingProject}); var conversionResult = await _joinableTaskFactory.RunAsync(async () => { var result = await ConvertDocumentUnhandledAsync(documentFilePath, selected, cancellationToken); - await WriteConvertedFilesAndShowSummaryAsync(new[] { result }.ToAsyncEnumerable()); + await WriteConvertedFilesAndShowSummaryAsync(new[] { result }.AsAsyncEnumerable()); return result; }); diff --git a/Vsix/CodeConverterPackage.cs b/Vsix/CodeConverterPackage.cs index e23f4253..cbcd058f 100644 --- a/Vsix/CodeConverterPackage.cs +++ b/Vsix/CodeConverterPackage.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.ComponentModelHost; +using Microsoft.VisualStudio.Extensibility; using Microsoft.VisualStudio.LanguageServices; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -85,6 +86,9 @@ public sealed class CodeConverterPackage : AsyncPackage public const string ConvertableSolutionMenuVisibilityGuid = "8e7192d0-28b7-4fe7-8d84-82c1db98d459"; internal Cancellation PackageCancellation { get; } = new(); + + internal static CodeConversion CodeConversionInstance { get; private set; } + internal static CodeConverterPackage Instance { get; private set; } /// /// Initializes a new instance of package class. @@ -109,6 +113,8 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); var visualStudioWorkspace = componentModel.GetService(); var codeConversion = await CodeConversion.CreateAsync(this, visualStudioWorkspace, this.GetDialogPageAsync); + CodeConversionInstance = codeConversion; + Instance = this; ConvertCSToVBCommand.Initialize(this, oleMenuCommandService, codeConversion); ConvertVBToCSCommand.Initialize(this, oleMenuCommandService, codeConversion); PasteAsVB.Initialize(this, oleMenuCommandService, codeConversion); diff --git a/Vsix/ConvertCSToVBExtensibilityCommand.cs b/Vsix/ConvertCSToVBExtensibilityCommand.cs new file mode 100644 index 00000000..3d1bc3a1 --- /dev/null +++ b/Vsix/ConvertCSToVBExtensibilityCommand.cs @@ -0,0 +1,45 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Extensibility; +using Microsoft.VisualStudio.Extensibility.Commands; + +namespace ICSharpCode.CodeConverter.VsExtension; + +[VisualStudioContribution] +public class ConvertCSToVBExtensibilityCommand : Command +{ + private readonly CodeConverterPackage _package; + + public ConvertCSToVBExtensibilityCommand(CodeConverterPackage package) + { + _package = package; + } + + public override CommandConfiguration CommandConfiguration => new("%a3378a21-e939-40c9-9e4b-eb0cec7b7854%"); + + public override async Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken) + { + var codeConversion = CodeConverterPackage.CodeConversionInstance; + var serviceProvider = CodeConverterPackage.Instance; + if (codeConversion == null || serviceProvider == null) return; + + var itemsPath = await VisualStudioInteraction.GetSelectedItemsPathAsync(CodeConversion.IsCSFileName); + if (itemsPath.Count > 0) + { + await codeConversion.ConvertDocumentsAsync(itemsPath, cancellationToken); + return; + } + + var projects = await VisualStudioInteraction.GetSelectedProjectsAsync(".csproj"); + if (projects.Count > 0) + { + await codeConversion.ConvertProjectsAsync(projects, cancellationToken); + return; + } + + (string filePath, var selection) = await VisualStudioInteraction.GetCurrentFilenameAndSelectionAsync(serviceProvider, CodeConversion.IsCSFileName, false); + if (filePath != null && selection != null) { + await codeConversion.ConvertDocumentAsync(filePath, selection.Value, cancellationToken); + } + } +} diff --git a/Vsix/ConvertVBToCSExtensibilityCommand.cs b/Vsix/ConvertVBToCSExtensibilityCommand.cs new file mode 100644 index 00000000..1cc9fe3e --- /dev/null +++ b/Vsix/ConvertVBToCSExtensibilityCommand.cs @@ -0,0 +1,45 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Extensibility; +using Microsoft.VisualStudio.Extensibility.Commands; + +namespace ICSharpCode.CodeConverter.VsExtension; + +[VisualStudioContribution] +public class ConvertVBToCSExtensibilityCommand : Command +{ + private readonly CodeConverterPackage _package; + + public ConvertVBToCSExtensibilityCommand(CodeConverterPackage package) + { + _package = package; + } + + public override CommandConfiguration CommandConfiguration => new("%a3378a21-e939-40c9-9e4b-eb0cec7b7854%"); + + public override async Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken) + { + var codeConversion = CodeConverterPackage.CodeConversionInstance; + var serviceProvider = CodeConverterPackage.Instance; + if (codeConversion == null || serviceProvider == null) return; + + var itemsPath = await VisualStudioInteraction.GetSelectedItemsPathAsync(CodeConversion.IsVBFileName); + if (itemsPath.Count > 0) + { + await codeConversion.ConvertDocumentsAsync(itemsPath, cancellationToken); + return; + } + + var projects = await VisualStudioInteraction.GetSelectedProjectsAsync(".vbproj"); + if (projects.Count > 0) + { + await codeConversion.ConvertProjectsAsync(projects, cancellationToken); + return; + } + + (string filePath, var selection) = await VisualStudioInteraction.GetCurrentFilenameAndSelectionAsync(serviceProvider, CodeConversion.IsVBFileName, false); + if (filePath != null && selection != null) { + await codeConversion.ConvertDocumentAsync(filePath, selection.Value, cancellationToken); + } + } +} diff --git a/Vsix/Extension.cs b/Vsix/Extension.cs new file mode 100644 index 00000000..5694252d --- /dev/null +++ b/Vsix/Extension.cs @@ -0,0 +1,12 @@ +using Microsoft.VisualStudio.Extensibility; + +namespace ICSharpCode.CodeConverter.VsExtension; + +[VisualStudioContribution] +public class CodeConverterExtension : Extension +{ + public override ExtensionConfiguration ExtensionConfiguration => new() { + RequiresInProcessHosting = true, + Metadata = null + }; +} diff --git a/Vsix/VisualStudioInteraction.cs b/Vsix/VisualStudioInteraction.cs index a1e49865..4ac8a818 100644 --- a/Vsix/VisualStudioInteraction.cs +++ b/Vsix/VisualStudioInteraction.cs @@ -32,8 +32,13 @@ internal static class VisualStudioInteraction { private static DTE2 _dte; - /// All calls and usages must be from the main thread> - internal static DTE2 Dte => _dte ??= Package.GetGlobalService(typeof(DTE)) as DTE2; + /// All calls and usages must be from the main thread + internal static DTE2 Dte { + get { + ThreadHelper.ThrowIfNotOnUIThread(); + return _dte ??= Package.GetGlobalService(typeof(DTE)) as DTE2; + } + } private static CancellationToken _cancelAllToken; private static readonly Version LowestSupportedVersion = new(16, 10, 0, 0); diff --git a/Vsix/Vsix.csproj b/Vsix/Vsix.csproj index 93d31a7a..02e64e82 100644 --- a/Vsix/Vsix.csproj +++ b/Vsix/Vsix.csproj @@ -1,145 +1,53 @@ - - + - 16.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - Debug - AnyCPU - 2.0 - ICSharpCode.CodeConverter.VsExtension - ICSharpCode.CodeConverter.VsExtension - {99498EF8-C9E0-433B-8D7B-EA8E9E66F0C7} - {82B43B9B-A64C-4715-B499-D71E9CA2BD60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - Library - v4.7.2 + net472 + disable + disable + 12 + true + + true true - true - true true true - Roslyn - Program - $(DevEnvDir)devenv.exe - /rootsuffix $(VSSDKTargetPlatformRegRootSuffix) - false - False - win - 12.0 - - - - true - true - true - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - $(LocalAppData)\Microsoft\VisualStudio\15.0_8bd9890a\Extensions\frc3cnfo.23z - True - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 + - - 1.8.4 - - - 10.0.2 - - + + + - - compile; build; native; contentfiles; analyzers; buildtransitive - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + + + + + + Menus.ctmenu - Designer - true PreserveNewest + true - true PreserveNewest + true Always true - - - - - - - - - - - Component - - - - - - - - - - - - - - - + true VSPackage - Designer - - - {7ea075c6-6406-445c-ab77-6c47aff88d58} - CodeConverter - - + - - - - - - - - - - - \ No newline at end of file diff --git a/Vsix/source.extension.vsixmanifest b/Vsix/source.extension.vsixmanifest index 961647bf..0325f30f 100644 --- a/Vsix/source.extension.vsixmanifest +++ b/Vsix/source.extension.vsixmanifest @@ -13,10 +13,10 @@ code converter vb csharp visual basic csharp net translate - + amd64 - + arm64