From ae13e233c4ec2140414265a3e73db2e599c7fa2b Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Wed, 24 Aug 2022 13:46:47 -0500 Subject: [PATCH 1/3] Use configured cadl-server path in VS when using a solution When a solution is open, there is no workspace, so we use the solution folder as the workspace folder and read the settings file ourselves. Also, some general cleanup: * Downgrade 16.x VS API packages to 16.0 for maximum compatibility * Upgrade other packages to latest * Run VS format command to format #if blocks not formatted by `dotnet format` * Run VS remove-and-sort-usings * Add inner exceptions to our exception types * Convert VariableResolver to static class * Avoid checking development mode in two places * Make helper types internal * Seal classes where possible * Use Dictionary.Add instead of indexer to get exception on duplicates * Use TryGetValue instead of catch KeyNotFoundException * Use exception filter instead of if+rethrow * Use string instead of string[] for process arguments * It was confusing me that we sometimes put more than one argument in a single array element. * Use `==` or `!= null` throughout instead of `is` and `is not` * I could go either way on this, but chose what we had most for consistency. --- .../solution-settings_2022-08-24-18-47.json | 10 + .../VS2019/Microsoft.Cadl.VS2019.csproj | 12 +- .../VS2022/Microsoft.Cadl.VS2022.csproj | 6 +- packages/cadl-vs/src/Exceptions.cs | 25 +-- packages/cadl-vs/src/VSExtension.cs | 209 ++++++++++++------ packages/cadl-vs/src/VariableResolver.cs | 42 +--- 6 files changed, 180 insertions(+), 124 deletions(-) create mode 100644 common/changes/cadl-vs/solution-settings_2022-08-24-18-47.json diff --git a/common/changes/cadl-vs/solution-settings_2022-08-24-18-47.json b/common/changes/cadl-vs/solution-settings_2022-08-24-18-47.json new file mode 100644 index 00000000000..fabdba529e0 --- /dev/null +++ b/common/changes/cadl-vs/solution-settings_2022-08-24-18-47.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "cadl-vs", + "comment": "Fix issue with configured cadl-server location not being found when opening a solution.", + "type": "patch" + } + ], + "packageName": "cadl-vs" +} \ No newline at end of file diff --git a/packages/cadl-vs/VS2019/Microsoft.Cadl.VS2019.csproj b/packages/cadl-vs/VS2019/Microsoft.Cadl.VS2019.csproj index d2b4a771440..04304a6779d 100644 --- a/packages/cadl-vs/VS2019/Microsoft.Cadl.VS2019.csproj +++ b/packages/cadl-vs/VS2019/Microsoft.Cadl.VS2019.csproj @@ -7,10 +7,14 @@ 17.0 - - - - + + + + + + + + diff --git a/packages/cadl-vs/VS2022/Microsoft.Cadl.VS2022.csproj b/packages/cadl-vs/VS2022/Microsoft.Cadl.VS2022.csproj index 6d14bd4047c..7f43791e59b 100644 --- a/packages/cadl-vs/VS2022/Microsoft.Cadl.VS2022.csproj +++ b/packages/cadl-vs/VS2022/Microsoft.Cadl.VS2022.csproj @@ -7,10 +7,14 @@ 18.0 + - + + + + diff --git a/packages/cadl-vs/src/Exceptions.cs b/packages/cadl-vs/src/Exceptions.cs index 50416d1befe..7891acb7de4 100644 --- a/packages/cadl-vs/src/Exceptions.cs +++ b/packages/cadl-vs/src/Exceptions.cs @@ -12,32 +12,25 @@ internal static class Win32ErrorCodes [Serializable] - public class CadlUserErrorException : Exception + internal class CadlUserErrorException : Exception { - public CadlUserErrorException() { } - - public CadlUserErrorException(string message) - : base(message) + public CadlUserErrorException(string message, Exception? innerException = null) + : base(message, innerException) { - } } - [Serializable] - public class CadlServerNotFoundException : CadlUserErrorException + internal sealed class CadlServerNotFoundException : CadlUserErrorException { - public CadlServerNotFoundException() { } - - public CadlServerNotFoundException(string name) + public CadlServerNotFoundException(string fileName, Exception? innerException = null) : base(string.Join("\n", new string[] { - $"Cadl server exectuable was not found: '{name}' is not found. Make sure either:", - " - cadl is installed globally with `npm install -g @cadl-lang/compiler'.", - " - cadl server path is configured with https://github.com/microsoft/cadl/blob/main/packages/cadl-vs/README.md#configure-cadl-visual-studio-extension." - })) + $"Cadl server exectuable was not found: '{fileName}' is not found. Make sure either:", + " - cadl is installed globally with `npm install -g @cadl-lang/compiler'.", + " - cadl server path is configured with https://github.com/microsoft/cadl/blob/main/packages/cadl-vs/README.md#configure-cadl-visual-studio-extension." + }, innerException)) { - } } } diff --git a/packages/cadl-vs/src/VSExtension.cs b/packages/cadl-vs/src/VSExtension.cs index c3b4cd7f687..cd3a3f7a209 100644 --- a/packages/cadl-vs/src/VSExtension.cs +++ b/packages/cadl-vs/src/VSExtension.cs @@ -1,22 +1,28 @@ +using EnvDTE; +using Microsoft.Cadl.VisualStudio; +using Microsoft.VisualStudio.LanguageServer.Client; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Threading; +using Microsoft.VisualStudio.Utilities; +using Microsoft.VisualStudio.Workspace; +using Microsoft.VisualStudio.Workspace.Settings; +using Microsoft.VisualStudio.Workspace.VSIntegration.Contracts; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; +using System.ComponentModel; using System.ComponentModel.Composition; using System.Diagnostics; using System.IO; +using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualStudio.Workspace; -using Microsoft.VisualStudio.Workspace.Settings; -using Microsoft.VisualStudio.Workspace.VSIntegration.Contracts; -using Microsoft.VisualStudio.LanguageServer.Client; -using Microsoft.VisualStudio.Shell; -using Microsoft.VisualStudio.Threading; -using Microsoft.VisualStudio.Utilities; +using Debugger = System.Diagnostics.Debugger; +using Process = System.Diagnostics.Process; using Task = System.Threading.Tasks.Task; -using System.Linq; -using System.ComponentModel; namespace Microsoft.Cadl.VisualStudio { @@ -41,59 +47,50 @@ public sealed class ContentDefinition [ContentType("cadl")] public sealed class LanguageClient : ILanguageClient { - public string Name => "Cadl"; public IEnumerable? ConfigurationSections { get; } = new[] { "cadl" }; - public object? InitializationOptions => null; public bool ShowNotificationOnInitializeFailed => true; public IEnumerable FilesToWatch { get; } = new[] { "**/*.cadl", "**/cadl-project.yaml", "**/package.json" }; public event AsyncEventHandler? StartAsync; public event AsyncEventHandler? StopAsync { add { } remove { } } // unused - private readonly IVsFolderWorkspaceService workspaceService; + private readonly IVsFolderWorkspaceService _workspaceService; + private readonly SVsServiceProvider _serviceProvider; + private string? _workspaceFolder; + private string? _configuredCadlServerPath; [ImportingConstructor] - public LanguageClient([Import] IVsFolderWorkspaceService workspaceService) + public LanguageClient( + [Import] IVsFolderWorkspaceService workspaceService, + [Import] SVsServiceProvider serviceProvider) { - this.workspaceService = workspaceService; + _workspaceService = workspaceService; + _serviceProvider = serviceProvider; } public async Task ActivateAsync(CancellationToken token) { - await Task.Yield(); + await LoadSettingsAsync(); - var workspace = workspaceService.CurrentWorkspace; - var settingsManager = workspace?.GetSettingsManager(); - var settings = settingsManager?.GetAggregatedSettings(SettingsTypes.Generic); - var options = Environment.GetEnvironmentVariable("CADL_SERVER_NODE_OPTIONS"); - var (serverCommand, serverArgs, env) = resolveCadlServer(settings); + var (serverCommand, serverArgs, env) = resolveCadlServer(); var info = new ProcessStartInfo { - // Use cadl-server on PATH in production FileName = serverCommand, - Arguments = string.Join(" ", serverArgs), + Arguments = serverArgs, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, - Environment = { new("NODE_OPTIONS", options) }, - WorkingDirectory = settings?.ScopePath, + WorkingDirectory = _workspaceFolder, }; foreach (var entry in env) { - info.Environment[entry.Key] = entry.Value; + info.Environment.Add(entry.Key, entry.Value); } -#if DEBUG - // Use local build of cadl-server in development (lauched from F5 in VS) - if (InDevelopmentMode()) - { - // --nolazy isn't supported by NODE_OPTIONS so we pass these via CLI instead - info.Environment.Remove("NODE_OPTIONS"); - } -#endif + try { var process = Process.Start(info); @@ -104,21 +101,16 @@ public LanguageClient([Import] IVsFolderWorkspaceService workspaceService) process.StandardOutput.BaseStream, process.StandardInput.BaseStream); } - catch (Win32Exception e) + catch (Win32Exception e) when (e.NativeErrorCode == Win32ErrorCodes.ERROR_FILE_NOT_FOUND) { - if (e.NativeErrorCode == Win32ErrorCodes.ERROR_FILE_NOT_FOUND) - { - throw new CadlServerNotFoundException(info.FileName); - } - throw e; + throw new CadlServerNotFoundException(info.FileName, e); } - } public async Task OnLoadedAsync() { var start = StartAsync; - if (start is not null) + if (start != null) { await start.InvokeAsync(this, EventArgs.Empty); } @@ -140,17 +132,23 @@ public Task OnServerInitializeFailedAsync(Exception e) #endif #if VS2022 - public Task OnServerInitializeFailedAsync(ILanguageClientInitializationInfo initializationState) { - var exception = initializationState.InitializationException; - var message = exception is CadlUserErrorException - ? exception.Message - : $"File issue at https://github.com/microsoft/cadl\r\n\r\n{exception}"; - Debug.Assert(exception is CadlUserErrorException, "Unexpected error initializing cadl-server:\r\n\r\n" + exception); - return Task.FromResult( - new InitializationFailureContext { - FailureMessage = "Failed to activate Cadl language server!\r\n" + message - }); - } + public Task OnServerInitializeFailedAsync(ILanguageClientInitializationInfo initializationState) + { + var exception = initializationState.InitializationException; + var message = exception is CadlUserErrorException + ? exception.Message + : $"File issue at https://github.com/microsoft/cadl\r\n\r\n{exception}"; + + Debug.Assert( + exception is CadlUserErrorException, + "Unexpected error initializing cadl-server:\r\n\r\n" + exception); + + return Task.FromResult( + new InitializationFailureContext + { + FailureMessage = "Failed to activate Cadl language server!\r\n" + message + }); + } #endif public Task OnServerInitializedAsync() @@ -160,7 +158,7 @@ public Task OnServerInitializedAsync() private void LogStderrMessage(string? message) { - if (message is null || message.Length == 0) + if (message == null || message.Length == 0) { return; } @@ -185,35 +183,45 @@ private static string GetDevelopmentCadlServerPath() // 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, "../compiler/cmd/cadl-server.js")); + return Path.GetFullPath(Path.Combine(srcDir, "..", "compiler", "cmd", "cadl-server.js")); } #endif - private (string, string[], IDictionary) resolveCadlServer(IWorkspaceSettings? settings) + private (string command, string arguments, IDictionary environment) resolveCadlServer() { var env = new Dictionary(); - var args = new string[] { "--stdio" }; + var args = "--stdio"; + var options = Environment.GetEnvironmentVariable("CADL_SERVER_NODE_OPTIONS"); + #if DEBUG // Use local build of cadl-server in development (lauched from F5 in VS) if (InDevelopmentMode()) { - var options = Environment.GetEnvironmentVariable("CADL_SERVER_NODE_OPTIONS"); - var module = GetDevelopmentCadlServerPath(); - return ("node.exe", new string[] { module, options }.Concat(args).ToArray(), env); + // NOTE: --no-lazy is not supported as environment variable, so we pass it in command line. + var module = GetDevelopmentCadlServerPath(); + return ("node.exe", $"{options} {module} {args}", env); } #endif + if (options != null && options.Length > 0) + { + env.Add("NODE_OPTIONS", options); + } - var serverPath = settings?.Property("cadl.cadl-server.path"); - if (serverPath == null) + var serverPath = _configuredCadlServerPath; + if (serverPath == null || serverPath.Length == 0) { return ("cadl-server.cmd", args, env); } var variables = new Dictionary(); - variables.Add("workspaceFolder", workspaceService.CurrentWorkspace.Location); - var variableResolver = new VariableResolver(variables); + if (_workspaceFolder != null && _workspaceFolder.Length > 0) + { + variables.Add("workspaceFolder", _workspaceFolder); + } + + serverPath = VariableResolver.ResolveVariables(serverPath, variables); + serverPath = Path.GetFullPath(serverPath); - serverPath = variableResolver.ResolveVariables(serverPath); if (!serverPath.EndsWith(".js")) { if (File.Exists(serverPath)) @@ -223,13 +231,80 @@ private static string GetDevelopmentCadlServerPath() } else { - serverPath = Path.Combine(serverPath, "cmd/cadl-server.js"); + serverPath = Path.Combine(serverPath, "cmd", "cadl-server.js"); } } - env["CADL_SKIP_COMPILER_RESOLVE"] = "1"; - return ("node.exe", new string[] { serverPath }.Concat(args).ToArray(), env); + // We need to check this as the later check when process is started would + // only trigger if node.exe is not found, not if the .js file passed to it + // is not found. + if (!File.Exists(serverPath)) + { + throw new CadlServerNotFoundException(serverPath); + } + + env.Add("CADL_SKIP_COMPILER_RESOLVE", "1"); + return ("node.exe", $"{serverPath} {args}", env); + } + + private async Task LoadSettingsAsync() + { + var workspace = _workspaceService.CurrentWorkspace; + if (workspace != null) + { + // Use workspace manager when there is a workspace. + var settings = workspace.GetSettingsManager()?.GetAggregatedSettings(SettingsTypes.Generic); + _configuredCadlServerPath = settings?.Property("cadl.cadl-server.path"); + _workspaceFolder = workspace.Location; + } + else + { + // When a solution is open, read the settings ourselves. + _workspaceFolder = await GetSolutionFolderAsync(); + var settings = ReadSettingsFromJson(_workspaceFolder); + settings.TryGetValue("cadl.cadl-server.path", out _configuredCadlServerPath); + } + } + + private static Dictionary ReadSettingsFromJson(string? workspaceFolder) + { + var empty = new Dictionary(); + if (workspaceFolder == null || workspaceFolder.Length == 0) + { + return empty; + } + + var settingsPath = Path.Combine(workspaceFolder, ".vs", "VSWorkspaceSettings.json"); + if (!File.Exists(settingsPath)) + { + return empty; + } + + try + { + var text = File.ReadAllText(settingsPath); + var json = JsonConvert.DeserializeObject>(text); + return json == null ? empty : json.Where((e) => e.Value is string).ToDictionary(e => e.Key, e => (string)e.Value); + } + catch (Exception e) when (e is IOException || e is UnauthorizedAccessException || e is JsonException) + { + throw new CadlUserErrorException($"Error reading {settingsPath}: {e.Message}", e); + } + } + + private async Task GetSolutionFolderAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = (DTE)_serviceProvider.GetService(typeof(DTE)); + + string? folder = null; + if (dte != null && dte.Solution != null) + { + folder = Path.GetDirectoryName(dte.Solution.FullName); + } + await TaskScheduler.Default; //return to thread pool thread + return folder; } } } diff --git a/packages/cadl-vs/src/VariableResolver.cs b/packages/cadl-vs/src/VariableResolver.cs index 189550b8dbe..6c733ef3eb6 100644 --- a/packages/cadl-vs/src/VariableResolver.cs +++ b/packages/cadl-vs/src/VariableResolver.cs @@ -1,52 +1,22 @@ -using System; using System.Collections.Generic; -using System.ComponentModel.Composition; -using System.Diagnostics; -using System.IO; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.VisualStudio.Workspace; -using Microsoft.VisualStudio.Workspace.Settings; -using Microsoft.VisualStudio.Workspace.VSIntegration.Contracts; -using Microsoft.VisualStudio.LanguageServer.Client; -using Microsoft.VisualStudio.Shell; -using Microsoft.VisualStudio.Threading; -using Microsoft.VisualStudio.Utilities; -using Task = System.Threading.Tasks.Task; -using System.Linq; -using System.ComponentModel; using System.Text.RegularExpressions; namespace Microsoft.Cadl.VisualStudio { - public class VariableResolver + internal static class VariableResolver { - public const string VARIABLE_REGEXP = @"\$\{(.*?)\}"; - private IDictionary variables; + private const string VARIABLE_REGEXP = @"\$\{(.*?)\}"; - public VariableResolver(IDictionary variables) - { - this.variables = variables; - } - public string ResolveVariables(string value) + public static string ResolveVariables(string value, IDictionary variables) { return Regex.Replace(value, VARIABLE_REGEXP, (match) => { var group = match.Groups[1]; - if (group == null) - { - return match.Value; - } - try - { - return variables[group.Value]; - } - catch (KeyNotFoundException) + if (group != null && variables.TryGetValue(group.Value, out value)) { - return match.Value; + return value; } + return match.Value; }); } } From 526e4379aa2efab918e5ce14a3beb063e9f41413 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Thu, 25 Aug 2022 11:11:57 -0500 Subject: [PATCH 2/3] Format whitespace --- packages/cadl-vs/src/Exceptions.cs | 2 +- packages/cadl-vs/src/VSExtension.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cadl-vs/src/Exceptions.cs b/packages/cadl-vs/src/Exceptions.cs index 7891acb7de4..22f794bb392 100644 --- a/packages/cadl-vs/src/Exceptions.cs +++ b/packages/cadl-vs/src/Exceptions.cs @@ -23,7 +23,7 @@ public CadlUserErrorException(string message, Exception? innerException = null) [Serializable] internal sealed class CadlServerNotFoundException : CadlUserErrorException { - public CadlServerNotFoundException(string fileName, Exception? innerException = null) + public CadlServerNotFoundException(string fileName, Exception? innerException = null) : base(string.Join("\n", new string[] { $"Cadl server exectuable was not found: '{fileName}' is not found. Make sure either:", diff --git a/packages/cadl-vs/src/VSExtension.cs b/packages/cadl-vs/src/VSExtension.cs index cd3a3f7a209..90e39fe8c20 100644 --- a/packages/cadl-vs/src/VSExtension.cs +++ b/packages/cadl-vs/src/VSExtension.cs @@ -273,7 +273,7 @@ private static Dictionary ReadSettingsFromJson(string? workspace { return empty; } - + var settingsPath = Path.Combine(workspaceFolder, ".vs", "VSWorkspaceSettings.json"); if (!File.Exists(settingsPath)) { From b2a6ba3efeb08d14ad989adb434877832938bf53 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Thu, 25 Aug 2022 11:19:02 -0500 Subject: [PATCH 3/3] Format --- packages/cadl-vs/src/VSExtension.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/cadl-vs/src/VSExtension.cs b/packages/cadl-vs/src/VSExtension.cs index 90e39fe8c20..42208a72bb9 100644 --- a/packages/cadl-vs/src/VSExtension.cs +++ b/packages/cadl-vs/src/VSExtension.cs @@ -136,18 +136,18 @@ public Task OnServerInitializeFailedAsync(Exception e) { var exception = initializationState.InitializationException; var message = exception is CadlUserErrorException - ? exception.Message - : $"File issue at https://github.com/microsoft/cadl\r\n\r\n{exception}"; + ? exception.Message + : $"File issue at https://github.com/microsoft/cadl\r\n\r\n{exception}"; Debug.Assert( exception is CadlUserErrorException, "Unexpected error initializing cadl-server:\r\n\r\n" + exception); return Task.FromResult( - new InitializationFailureContext - { - FailureMessage = "Failed to activate Cadl language server!\r\n" + message - }); + new InitializationFailureContext + { + FailureMessage = "Failed to activate Cadl language server!\r\n" + message + }); } #endif