From 33cf9b6dca7f119cee7fb20a2894aab75772783a Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Thu, 8 Apr 2021 16:52:26 -0700 Subject: [PATCH] Log output from ADL language server to VS output window pane (#437) * Log output from ADL language server to VS output window pane * Use nullable * Turn up warnings; treat warnings as errors * Launch petstore sample on F5 to reduce iteration time --- packages/adl-vs/.gitignore | 6 - .../adl-vs/Microsoft.Adl.VisualStudio.csproj | 15 +- .../adl-vs/Microsoft.Adl.VisualStudio.sln | 25 +++ .../adl-vs/Properties/launchSettings.json | 4 +- packages/adl-vs/src/VSExtension.cs | 187 ++++++++++++++++-- 5 files changed, 206 insertions(+), 31 deletions(-) delete mode 100644 packages/adl-vs/.gitignore create mode 100644 packages/adl-vs/Microsoft.Adl.VisualStudio.sln diff --git a/packages/adl-vs/.gitignore b/packages/adl-vs/.gitignore deleted file mode 100644 index 0d3b48f2a..000000000 --- a/packages/adl-vs/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -bin/ -obj/ -.vs/ -*.binlog -*.sln - diff --git a/packages/adl-vs/Microsoft.Adl.VisualStudio.csproj b/packages/adl-vs/Microsoft.Adl.VisualStudio.csproj index cff5b3077..f6be93d24 100644 --- a/packages/adl-vs/Microsoft.Adl.VisualStudio.csproj +++ b/packages/adl-vs/Microsoft.Adl.VisualStudio.csproj @@ -1,4 +1,4 @@ - + net472 @@ -8,14 +8,19 @@ false false Latest + Enable + true + 5 + strict - - + + - + - + + diff --git a/packages/adl-vs/Microsoft.Adl.VisualStudio.sln b/packages/adl-vs/Microsoft.Adl.VisualStudio.sln new file mode 100644 index 000000000..a3fdbc56f --- /dev/null +++ b/packages/adl-vs/Microsoft.Adl.VisualStudio.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31202.260 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Adl.VisualStudio", "Microsoft.Adl.VisualStudio.csproj", "{E56D4BDB-4A17-4BBB-81C1-258FBA49979F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E56D4BDB-4A17-4BBB-81C1-258FBA49979F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E56D4BDB-4A17-4BBB-81C1-258FBA49979F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E56D4BDB-4A17-4BBB-81C1-258FBA49979F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E56D4BDB-4A17-4BBB-81C1-258FBA49979F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1DFC3838-4459-4321-B2A1-D473D4654597} + EndGlobalSection +EndGlobal diff --git a/packages/adl-vs/Properties/launchSettings.json b/packages/adl-vs/Properties/launchSettings.json index 5180c04bb..81f446102 100644 --- a/packages/adl-vs/Properties/launchSettings.json +++ b/packages/adl-vs/Properties/launchSettings.json @@ -2,8 +2,8 @@ "profiles": { "Microsoft.Adl.VisualStudio": { "commandName": "Executable", - "executablePath": "devenv.exe", - "commandLineArgs": "/rootSuffix Exp", + "executablePath": "$(DevEnvDir)devenv.exe", + "commandLineArgs": "/rootSuffix Exp ../../../adl-samples/petstore", "environmentVariables": { "ADL_SERVER_NODE_OPTIONS": "--nolazy --inspect=4242", "ADL_DEVELOPMENT_MODE": "true" diff --git a/packages/adl-vs/src/VSExtension.cs b/packages/adl-vs/src/VSExtension.cs index 4c78c7ec2..c63ff27c5 100644 --- a/packages/adl-vs/src/VSExtension.cs +++ b/packages/adl-vs/src/VSExtension.cs @@ -7,39 +7,49 @@ using System.Reflection; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using Task = System.Threading.Tasks.Task; using Microsoft.VisualStudio.LanguageServer.Client; using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.Threading; using Microsoft.VisualStudio.Utilities; +using Task = System.Threading.Tasks.Task; +using IAsyncServiceProvider = Microsoft.VisualStudio.Shell.IAsyncServiceProvider; + namespace Microsoft.Adl.VisualStudio { [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] [Guid("88b9492f-c019-492c-8aeb-f325a7e4cf23")] public sealed class Package : AsyncPackage { } - public class ContentDefinition { + public sealed class ContentDefinition { [Export] [Name("adl")] [BaseDefinition(CodeRemoteContentDefinition.CodeRemoteContentTypeName)] - public static ContentTypeDefinition AdlContentTypeDefinition; + public ContentTypeDefinition? AdlContentTypeDefinition => null; [Export] [FileExtension(".adl")] [ContentType("adl")] - public static FileExtensionToContentTypeDefinition AdlFileExtensionDefinition; + public FileExtensionToContentTypeDefinition? AdlFileExtensionDefinition => null; } - [ContentType("adl")] [Export(typeof(ILanguageClient))] - public class LanguageClient : ILanguageClient { + [ContentType("adl")] + public sealed class LanguageClient : ILanguageClient { public string Name => "ADL Language Support"; - public IEnumerable ConfigurationSections => null; - public object InitializationOptions => null; - public IEnumerable FilesToWatch => null; - public event AsyncEventHandler StartAsync; - public event AsyncEventHandler StopAsync { add { } remove { } } // unused + public IEnumerable? ConfigurationSections => null; + public object? InitializationOptions => null; + public IEnumerable? FilesToWatch => null; + public event AsyncEventHandler? StartAsync; + public event AsyncEventHandler? StopAsync { add { } remove { } } // unused + + private readonly IOutputWindow _outputWindow; + + [ImportingConstructor] + public LanguageClient(IOutputWindow window) { + _outputWindow = window; + } public async Task ActivateAsync(CancellationToken token) { await Task.Yield(); @@ -51,6 +61,7 @@ namespace Microsoft.Adl.VisualStudio { Arguments = "--stdio", RedirectStandardInput = true, RedirectStandardOutput = true, + RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, Environment = { new("NODE_OPTIONS", options) }, @@ -68,11 +79,19 @@ namespace Microsoft.Adl.VisualStudio { #endif var process = Process.Start(info); - return new Connection(process.StandardOutput.BaseStream, process.StandardInput.BaseStream); + process.BeginErrorReadLine(); + process.ErrorDataReceived += (_, e) => LogMessage(e.Data); + + return new Connection( + process.StandardOutput.BaseStream, + process.StandardInput.BaseStream); } public async Task OnLoadedAsync() { - await StartAsync?.InvokeAsync(this, EventArgs.Empty); + var start = StartAsync; + if (start is not null) { + await start.InvokeAsync(this, EventArgs.Empty); + } } public Task OnServerInitializeFailedAsync(Exception e) { @@ -83,22 +102,154 @@ namespace Microsoft.Adl.VisualStudio { return Task.CompletedTask; } + private void LogMessage(string? message) { + if (message is null || message.Length == 0) { + return; + } + + Debug.WriteLine("ADL Server: " + message); + _outputWindow.LogMessage(message); + } + #if DEBUG - static bool InDevelopmentMode() { + private static bool InDevelopmentMode() { return string.Equals( Environment.GetEnvironmentVariable("ADL_DEVELOPMENT_MODE"), "true", StringComparison.OrdinalIgnoreCase); } - static string GetDevelopmentAdlServerPath() { - // Even when debugging, we get deployed to an extension folder outside the source - // tree, so we stash the source directory in a file in debug builds so we can use it - // to run adl-server against the live developer build in the source tree. + private static string GetDevelopmentAdlServerPath() { + // Even when debugging, we get deployed to an extension folder outside the + // source tree, so we stash the source directory in a file in debug builds + // so we can use it to run adl-server against the live developer build in + // the source tree. var thisDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); var srcDir = File.ReadAllText(Path.Combine(thisDir, "DebugSourceDirectory.txt")).Trim(); return Path.GetFullPath(Path.Combine(srcDir, "../adl/cmd/adl-server.js")); } #endif } + + public interface IOutputWindow { + void LogMessage(string message); + } + + [Export(typeof(IOutputWindow))] + internal sealed class OutputWindow : IOutputWindow { + private static readonly Guid _guid = new("{2C6CA609-4EC9-4AEE-B163-AFF26503CAA4}"); + private readonly IAsyncServiceProvider _serviceProvider; + private readonly JoinableTaskContext _context; + private readonly AsyncLazy _pane; + + [ImportingConstructor] + public OutputWindow( + [Import(typeof(SAsyncServiceProvider))] + IAsyncServiceProvider serviceProvider, + JoinableTaskContext context + ) { + _serviceProvider = serviceProvider; + _context = context; + _pane = new AsyncLazy(CreateOutputWindowAsync, context.Factory); + } + + public void LogMessage(string message) { + message += Environment.NewLine; + + // If the volume of log messages ever gets high, we'd need to batch them + // to avoid creating too much pending async work by doing this threading + // dance for every message. + Task.Run(async () => { + await _context.Factory.SwitchToMainThreadAsync(); + + var pane = await _pane.GetValueAsync(); + if (pane is null) { + return; + } + + if (pane is IVsOutputWindowPaneNoPump paneNoPump) { + paneNoPump.OutputStringNoPump(message); + } else { + var hr = pane.OutputStringThreadSafe(message); + if (Failed(hr)) { + return; + } + } + }).Forget(); + } + + private async Task CreateOutputWindowAsync() { + await _context.Factory.SwitchToMainThreadAsync(); + + var window = await _serviceProvider.GetServiceAsync(); + if (window is null) { + return null; + } + + // Creating an output window pane activates it, and we don't want to do + // that, so we have to take steps to restore the active pane here. + var activePane = GetActivePane(window); + + var pane = CreateOutputWindowPane(window); + if (pane is null) { + return null; + } + + if (activePane != Guid.Empty) { + ActivatePane(window, activePane); + } + + return pane; + } + + private static void ActivatePane(IVsOutputWindow window, Guid guid) { + ThreadHelper.ThrowIfNotOnUIThread(); + + var hr = window.GetPane(ref guid, out var pane); + if (Failed(hr)) { + return; + } + + hr = pane.Activate(); + if (Failed(hr)) { + return; + } + } + + private static Guid GetActivePane(IVsOutputWindow window) { + ThreadHelper.ThrowIfNotOnUIThread(); + + if (window is not IVsOutputWindow2 window2) { + return Guid.Empty; + } + + var hr = window2.GetActivePaneGUID(out var guid); + if (Failed(hr)) { + return Guid.Empty; + } + + return guid; + } + + private static IVsOutputWindowPane? CreateOutputWindowPane(IVsOutputWindow window) { + ThreadHelper.ThrowIfNotOnUIThread(); + + var guid = _guid; + var hr = window.CreatePane(ref guid, "ADL", 1, 0); + if (Failed(hr)) { + return null; + } + + hr = window.GetPane(ref guid, out var pane); + if (Failed(hr)) { + return null; + } + + return pane; + } + + private static bool Failed(int hr) { + return hr < 0; + } + } }