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..22f794bb392 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..42208a72bb9 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;
- }
-#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");
+ info.Environment.Add(entry.Key, entry.Value);
}
-#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;
});
}
}