diff --git a/NuGet.config b/NuGet.config index e0e61bbbfd..687b1bf8e6 100644 --- a/NuGet.config +++ b/NuGet.config @@ -6,6 +6,7 @@ + diff --git a/Roslyn-SDK.sln b/Roslyn-SDK.sln index 2715ebfa91..ef1484fb0b 100644 --- a/Roslyn-SDK.sln +++ b/Roslyn-SDK.sln @@ -189,6 +189,8 @@ Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "Microsoft.CodeAnalysis.Visu EndProject Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "Microsoft.CodeAnalysis.VisualBasic.SourceGenerators.Testing.XUnit.UnitTests", "tests\Microsoft.CodeAnalysis.Testing\Microsoft.CodeAnalysis.VisualBasic.SourceGenerators.Testing.XUnit.UnitTests\Microsoft.CodeAnalysis.VisualBasic.SourceGenerators.Testing.XUnit.UnitTests.vbproj", "{92BD1781-5DB4-4F72-BCCB-0D64C0790A2B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Roslyn.ComponentDebugger", "src\VisualStudio.Roslyn.SDK\ComponentDebugger\Roslyn.ComponentDebugger.csproj", "{421DE59C-8246-4679-9D69-79F16A7187BE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -539,6 +541,10 @@ Global {92BD1781-5DB4-4F72-BCCB-0D64C0790A2B}.Debug|Any CPU.Build.0 = Debug|Any CPU {92BD1781-5DB4-4F72-BCCB-0D64C0790A2B}.Release|Any CPU.ActiveCfg = Release|Any CPU {92BD1781-5DB4-4F72-BCCB-0D64C0790A2B}.Release|Any CPU.Build.0 = Release|Any CPU + {421DE59C-8246-4679-9D69-79F16A7187BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {421DE59C-8246-4679-9D69-79F16A7187BE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {421DE59C-8246-4679-9D69-79F16A7187BE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {421DE59C-8246-4679-9D69-79F16A7187BE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -635,6 +641,7 @@ Global {7D9C0EF5-7383-4E35-811B-3288B3C806F3} = {9905147E-CC1F-42A0-BD27-05586C583DF7} {7C3FE60E-055B-4E0C-BB85-C7E94A640074} = {9905147E-CC1F-42A0-BD27-05586C583DF7} {92BD1781-5DB4-4F72-BCCB-0D64C0790A2B} = {9905147E-CC1F-42A0-BD27-05586C583DF7} + {421DE59C-8246-4679-9D69-79F16A7187BE} = {F9B73995-76C6-4056-ADA9-18342F951361} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {56695AA9-EA80-47A7-8562-E51285906C54} diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/CommandLineArgumentsDataSource.cs b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/CommandLineArgumentsDataSource.cs new file mode 100644 index 0000000000..64174d3a25 --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/CommandLineArgumentsDataSource.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.ProjectSystem.VS; + +namespace Roslyn.ComponentDebugger +{ + [Export] + [AppliesTo("(" + ProjectCapabilities.CSharp + " | " + ProjectCapabilities.VB + ") & !" + ProjectCapabilities.SharedAssetsProject)] + public class CommandLineArgumentsDataSource : UnconfiguredProjectHostBridge, IProjectVersionedValue>, IProjectVersionedValue>> + { + private readonly IActiveConfiguredProjectSubscriptionService _activeProjectSubscriptionService; + + [ImportingConstructor] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "MEF ensures not null")] + public CommandLineArgumentsDataSource(IProjectThreadingService projectThreadingService, IActiveConfiguredProjectSubscriptionService activeProjectSubscriptionService) + : base(projectThreadingService.JoinableTaskContext) + { + _activeProjectSubscriptionService = activeProjectSubscriptionService; + } + + public async Task> GetArgsAsync() + { + using (JoinableCollection.Join()) + { + await this.InitializeAsync().ConfigureAwait(true); + return this.AppliedValue?.Value ?? ImmutableArray.Empty; + } + } + + protected override bool BlockInitializeOnFirstAppliedValue => true; + + protected override Task InitializeInnerCoreAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + protected override IDisposable LinkExternalInput(ITargetBlock> targetBlock) + { + JoinUpstreamDataSources(_activeProjectSubscriptionService.ProjectBuildRuleSource); + return _activeProjectSubscriptionService.ProjectBuildRuleSource.SourceBlock.LinkTo(target: targetBlock, + linkOptions: new DataflowLinkOptions { PropagateCompletion = true }, + initialDataAsNew: true, + suppressVersionOnlyUpdates: true, + ruleNames: Constants.CommandLineArgsRuleName); + } + + protected override Task>> PreprocessAsync(IProjectVersionedValue input, IProjectVersionedValue>? previousOutput) + { + if (input is null) + { + throw new ArgumentNullException(nameof(input)); + } + + var description = input.Value.ProjectChanges[Constants.CommandLineArgsRuleName]; + return Task.FromResult>>(new ProjectVersionedValue>(description.After.Items.Keys.ToImmutableArray(), input.DataSourceVersions)); + } + + protected override Task ApplyAsync(IProjectVersionedValue> value) + { + AppliedValue = value; + return Task.CompletedTask; + } + } +} diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/Constants.cs b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/Constants.cs new file mode 100644 index 0000000000..3c8bb093d3 --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/Constants.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Roslyn.ComponentDebugger +{ + internal static class Constants + { + public const string RoslynComponentCapability = "RoslynComponent"; + + public const string CommandName = "DebugRoslynComponent"; + + public const string TargetProjectKeyName = "targetProject"; + + public const string CommandLineArgsRuleName = "CompilerCommandLineArgs"; + } +} diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/DebugProfileProvider.cs b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/DebugProfileProvider.cs new file mode 100644 index 0000000000..5f2ca2b3e9 --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/DebugProfileProvider.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.ProjectSystem.Debug; +using Microsoft.VisualStudio.ProjectSystem.VS.Debug; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Threading; +using Task = System.Threading.Tasks.Task; + +namespace Roslyn.ComponentDebugger +{ + [Export(typeof(IDebugProfileLaunchTargetsProvider))] + [AppliesTo(Constants.RoslynComponentCapability)] + public class DebugProfileProvider : IDebugProfileLaunchTargetsProvider + { + private readonly ConfiguredProject _configuredProject; + private readonly LaunchSettingsManager _launchSettingsManager; + private readonly IProjectThreadingService _threadingService; + private readonly AsyncLazy _compilerRoot; + + [ImportingConstructor] + [Obsolete("This exported object must be obtained through the MEF export provider.", error: true)] + public DebugProfileProvider(ConfiguredProject configuredProject, LaunchSettingsManager launchSettingsManager, SVsServiceProvider? serviceProvider, IProjectThreadingService threadingService) + { + _configuredProject = configuredProject; + _launchSettingsManager = launchSettingsManager; + _threadingService = threadingService; + + _compilerRoot = new AsyncLazy(() => GetCompilerRootAsync(serviceProvider), _threadingService.JoinableTaskFactory); + } + + public Task OnAfterLaunchAsync(DebugLaunchOptions launchOptions, ILaunchProfile profile) => Task.CompletedTask; + + public Task OnBeforeLaunchAsync(DebugLaunchOptions launchOptions, ILaunchProfile profile) => Task.CompletedTask; + + public bool SupportsProfile(ILaunchProfile? profile) => Constants.CommandName.Equals(profile?.CommandName, StringComparison.Ordinal); + + public async Task> QueryDebugTargetsAsync(DebugLaunchOptions launchOptions, ILaunchProfile? profile) + { + // set up the managed (net fx) debugger to start a process + // https://github.com/dotnet/roslyn-sdk/issues/729 + var settings = new DebugLaunchSettings(launchOptions) + { + LaunchDebugEngineGuid = Microsoft.VisualStudio.ProjectSystem.Debug.DebuggerEngines.ManagedOnlyEngine, + LaunchOperation = DebugLaunchOperation.CreateProcess + }; + + var compilerRoot = await _compilerRoot.GetValueAsync().ConfigureAwait(true); + if (compilerRoot is object) + { + // try and get the target project + var targetProjectUnconfigured = await _launchSettingsManager.TryGetProjectForLaunchAsync(profile).ConfigureAwait(true); + if (targetProjectUnconfigured is object) + { + settings.CurrentDirectory = Path.GetDirectoryName(targetProjectUnconfigured.FullPath); + var compiler = _configuredProject.Capabilities.Contains(ProjectCapabilities.VB) ? "vbc.exe" : "csc.exe"; + settings.Executable = Path.Combine(compilerRoot, compiler); + + // get its compilation args + var args = await targetProjectUnconfigured.GetCompilationArgumentsAsync().ConfigureAwait(true); + + // append the command line args to the debugger launch + settings.Arguments = string.Join(" ", args); + } + } + // https://github.com/dotnet/roslyn-sdk/issues/728 : better error handling + return new IDebugLaunchSettings[] { settings }; + } + + private async Task GetCompilerRootAsync(SVsServiceProvider? serviceProvider) + { + await _threadingService.SwitchToUIThread(); + + // https://github.com/dotnet/roslyn-sdk/issues/729 : don't hardcode net fx compiler + var shell = (IVsShell?)serviceProvider?.GetService(typeof(SVsShell)); + if (shell is object + && shell.GetProperty((int)__VSSPROPID2.VSSPROPID_InstallRootDir, out var rootDirObj) == VSConstants.S_OK + && rootDirObj is string rootDir) + { + return Path.Combine(rootDir, "MSBuild", "Current", "Bin", "Roslyn"); + } + + return null; + } + } +} diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/DebuggerOptions.xaml b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/DebuggerOptions.xaml new file mode 100644 index 0000000000..b431658e46 --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/DebuggerOptions.xaml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/DebuggerOptionsViewModel.cs b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/DebuggerOptionsViewModel.cs new file mode 100644 index 0000000000..180012a97e --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/DebuggerOptionsViewModel.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Roslyn.ComponentDebugger +{ + internal sealed class DebuggerOptionsViewModel : INotifyPropertyChanged + { + private readonly Action _indexChanged; + + private IEnumerable _projectNames = ImmutableArray.Empty; + + private int _selectedProjectIndex = -1; + + public event PropertyChangedEventHandler? PropertyChanged; + + public DebuggerOptionsViewModel(Action indexChanged) + { + _indexChanged = indexChanged; + } + + public IEnumerable ProjectNames + { + get => _projectNames; + set + { + if (!_projectNames.SequenceEqual(value)) + { + _projectNames = value; + NotifyPropertyChanged(); + } + } + } + + public int SelectedProjectIndex + { + get => _selectedProjectIndex; + set + { + if (_selectedProjectIndex != value) + { + _selectedProjectIndex = value; + NotifyPropertyChanged(); + _indexChanged?.Invoke(value); + } + } + } + + private void NotifyPropertyChanged([CallerMemberName]string propertyName = "") + { + this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/LaunchSettingsManager.cs b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/LaunchSettingsManager.cs new file mode 100644 index 0000000000..6d9c972364 --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/LaunchSettingsManager.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.ComponentModel.Composition; +using System.Threading.Tasks; +using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.ProjectSystem.Debug; + +namespace Roslyn.ComponentDebugger +{ + [Export] + public class LaunchSettingsManager + { + private readonly UnconfiguredProject _owningProject; + private readonly IDebugTokenReplacer _tokenReplacer; + + [ImportingConstructor] + public LaunchSettingsManager(UnconfiguredProject owningProject, IDebugTokenReplacer tokenReplacer) + { + _owningProject = owningProject; + _tokenReplacer = tokenReplacer; + } + + public async Task TryGetProjectForLaunchAsync(ILaunchProfile? profile) + { + UnconfiguredProject? targetProject = null; + object? value = null; + profile?.OtherSettings?.TryGetValue(Constants.TargetProjectKeyName, out value); + + if (value is string targetProjectPath) + { + // expand any variables in the path, and root it based on this project + var replacedProjectPath = await _tokenReplacer.ReplaceTokensInStringAsync(targetProjectPath, true).ConfigureAwait(true); + replacedProjectPath = _owningProject.MakeRooted(replacedProjectPath); + + targetProject = ((IProjectService2)_owningProject.Services.ProjectService).GetLoadedProject(replacedProjectPath); + } + return targetProject; + } + + public void WriteProjectForLaunch(IWritableLaunchProfile profile, UnconfiguredProject targetProject) + { + if (profile is null) + { + throw new System.ArgumentNullException(nameof(profile)); + } + + if (targetProject is null) + { + throw new System.ArgumentNullException(nameof(targetProject)); + } + + var rootedPath = _owningProject.MakeRelative(targetProject.FullPath); + profile.OtherSettings[Constants.TargetProjectKeyName] = rootedPath; + } + + } +} diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/LaunchSettingsProvider.cs b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/LaunchSettingsProvider.cs new file mode 100644 index 0000000000..43b54a2d7f --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/LaunchSettingsProvider.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; +using System.Windows.Controls; +using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.ProjectSystem.Debug; +using Microsoft.VisualStudio.Utilities; +using Task = System.Threading.Tasks.Task; + +namespace Roslyn.ComponentDebugger +{ + [Export(typeof(ILaunchSettingsUIProvider))] + [AppliesTo(Constants.RoslynComponentCapability)] + public class LaunchSettingsProvider : ILaunchSettingsUIProvider + { + private readonly IProjectThreadingService _threadingService; + private readonly UnconfiguredProject _unconfiguredProject; + private readonly LaunchSettingsManager _launchSettingsManager; + private readonly DebuggerOptionsViewModel _viewModel; + + private ImmutableArray _projects; + private IWritableLaunchProfile? _launchProfile; + + [ImportingConstructor] + [Obsolete("This exported object must be obtained through the MEF export provider.", error: true)] + public LaunchSettingsProvider(IProjectThreadingService threadingService, UnconfiguredProject unconfiguredProject, LaunchSettingsManager launchSettingsManager) + { + _threadingService = threadingService; + _unconfiguredProject = unconfiguredProject; + _launchSettingsManager = launchSettingsManager; + _viewModel = new DebuggerOptionsViewModel(IndexChanged); + } + + public string CommandName { get => Constants.CommandName; } + + // https://github.com/dotnet/roslyn-sdk/issues/730 : localization + public string FriendlyName { get => "Roslyn Component"; } + + public UserControl? CustomUI { get => new DebuggerOptions() { DataContext = _viewModel }; } + + public void ProfileSelected(IWritableLaunchSettings curSettings) + { + _launchProfile = curSettings?.ActiveProfile; + _threadingService.ExecuteSynchronously(UpdateViewModelAsync); + } + + public bool ShouldEnableProperty(string propertyName) + { + // we disable all the default options for a debugger. + // in the future we might want to enable env vars and (potentially) the exe to allow + // customization of the compiler used? + return false; + } + + private async Task UpdateViewModelAsync() + { + var targetProjects = ArrayBuilder.GetInstance(); + + // get the output assembly for this project + var projectArgs = await _unconfiguredProject.GetCompilationArgumentsAsync().ConfigureAwait(false); + var targetArg = projectArgs.LastOrDefault(a => a.StartsWith("/out:", StringComparison.OrdinalIgnoreCase)); + var target = Path.GetFileName(targetArg); + + var projectService = _unconfiguredProject.Services.ProjectService; + foreach (var targetProjectUnconfigured in projectService.LoadedUnconfiguredProjects) + { + // check if the args contain the project as an analyzer ref + foreach (var arg in await targetProjectUnconfigured.GetCompilationArgumentsAsync().ConfigureAwait(false)) + { + if (arg.StartsWith("/analyzer:", StringComparison.OrdinalIgnoreCase) + && arg.EndsWith(target, StringComparison.OrdinalIgnoreCase)) + { + targetProjects.Add(targetProjectUnconfigured); + } + } + } + _projects = targetProjects.ToImmutableAndFree(); + + var launchTargetProject = await _launchSettingsManager.TryGetProjectForLaunchAsync(_launchProfile?.ToLaunchProfile()).ConfigureAwait(true); + var index = _projects.IndexOf(launchTargetProject!); + + _viewModel.ProjectNames = _projects.Select(p => Path.GetFileNameWithoutExtension(p.FullPath)); + _viewModel.SelectedProjectIndex = index; + } + + private void IndexChanged(int newIndex) + { + if (_launchProfile is object && !_projects.IsDefaultOrEmpty && newIndex >= 0 && newIndex < _projects.Length) + { + var project = _projects[newIndex]; + _launchSettingsManager.WriteProjectForLaunch(_launchProfile, project); + } + } + } +} diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/ProjectUtilities.cs b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/ProjectUtilities.cs new file mode 100644 index 0000000000..b20cc31159 --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/ProjectUtilities.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Microsoft.VisualStudio.ProjectSystem; + +namespace Roslyn.ComponentDebugger +{ + public static class ProjectUtilities + { + public static Task> GetCompilationArgumentsAsync(this UnconfiguredProject project) + { + if (project is null) + { + throw new ArgumentNullException(nameof(project)); + } + + var dataSource = project.Services.ExportProvider.GetExportedValueOrDefault(); + if (dataSource is object) + { + return dataSource.GetArgsAsync(); + } + + return Task.FromResult(ImmutableArray.Empty); + } + } +} diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/Roslyn.ComponentDebugger.csproj b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/Roslyn.ComponentDebugger.csproj new file mode 100644 index 0000000000..37bb4c8b98 --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/Roslyn.ComponentDebugger.csproj @@ -0,0 +1,40 @@ + + + + + Library + Roslyn.ComponentDebugger + net472 + enable + NU1603;NU1605 + + + + + + + + + + + + + + + + + + + + + + + + + + + + MSBuild:Compile + + + diff --git a/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/Roslyn.SDK.csproj b/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/Roslyn.SDK.csproj index b04a9074b2..370c4ea744 100644 --- a/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/Roslyn.SDK.csproj +++ b/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/Roslyn.SDK.csproj @@ -120,5 +120,15 @@ true false + + Roslyn.ComponentDebugger + BuiltProjectOutputGroup%3bGetCopyToOutputDirectoryItems%3b + DebugSymbolsProjectOutputGroup%3b + true + false + + + + diff --git a/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/source.extension.vsixmanifest b/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/source.extension.vsixmanifest index 8d8661a4ee..a091d5313b 100644 --- a/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/source.extension.vsixmanifest +++ b/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/source.extension.vsixmanifest @@ -30,6 +30,7 @@ +