diff --git a/src/RustAnalyzer/Infrastructure/Options.cs b/src/RustAnalyzer/Infrastructure/Options.cs index efbf4ed..6ea0064 100644 --- a/src/RustAnalyzer/Infrastructure/Options.cs +++ b/src/RustAnalyzer/Infrastructure/Options.cs @@ -1,6 +1,10 @@ using System.ComponentModel; +using System.Drawing.Design; +using System.IO; using System.Runtime.InteropServices; using Community.VisualStudio.Toolkit; +using Microsoft.VisualStudio.Shell; +using Newtonsoft.Json.Linq; namespace KS.RustAnalyzer.Infrastructure; @@ -27,6 +31,12 @@ public interface ISettingsServiceDefaults public string AdditionalTestExecutionArguments { get; set; } public string TestExecutionEnvironment { get; set; } + + public string LspInitializationOptions { get; set; } + + public string RustAnalyzerEnvArguments { get; set; } + + public bool EnableRustAnalyzerStderrLogging { get; set; } } public class Options : BaseOptionModel, ISettingsServiceDefaults @@ -82,6 +92,131 @@ public class Options : BaseOptionModel, ISettingsServiceDefaults [Browsable(true)] [Category(SettingsInfo.KindTest)] [DisplayName("Execution environment")] - [Description($"Additioanal environment variables to set for test execution. Default: RUST_BACKTRACE=full. Example: RUST_BACKTRACE=1.")] + [Description($"Additional environment variables to set for test execution. Default: RUST_BACKTRACE=full. Example: RUST_BACKTRACE=1.")] public string TestExecutionEnvironment { get; set; } = "RUST_BACKTRACE=full"; + + [Browsable(true)] + [Category(SettingsInfo.KindConfig)] + [DisplayName("LSP Initialization Options")] + [Description("JSON object for LSP initializationOptions, see https://rust-analyzer.github.io/book/configuration.\n" + + "You can override this global setting by placing a file named 'lsp_initializationOptions.json' in your top-level project directory. " + + "The two JSON objects will be deep-merged.\n" + + "Make sure the JSON is valid and strings are correctly escaped (e.g., use '\\\\' or just '/' for paths in Windows).\n" + + "Reopening the Solution is required for changes to this setting to take effect.")] + [Editor(typeof(System.ComponentModel.Design.MultilineStringEditor), typeof(UITypeEditor))] + public string LspInitializationOptions { get; set; } = "{}"; + + public JObject GetMergedLspInitializationOptions(string projectRootDir) + { + var configString = LspInitializationOptions; + var lspInitOpsPath = !string.IsNullOrEmpty(projectRootDir) ? Path.Combine(projectRootDir, "lsp_initializationOptions.json") : null; + var globalInitOps = new JObject(); + var localInitOps = new JObject(); + + if (!string.IsNullOrWhiteSpace(configString)) + { + try + { + globalInitOps = JObject.Parse(configString); + } + catch (System.Exception ex) + { + RustAnalyzerPackage.JTF.RunAsync(async () => + { + await VsCommon.ShowErrorMessageAsync( + "Rust Analyzer", + $"Error parsing LSP initialization options from settings: {ex.Message}\n\nPlease check your LSP initialization options in Tools > Options > Rust Analyzer > General."); + }).FireAndForget(); + globalInitOps = new JObject(); + } + } + + if (!string.IsNullOrEmpty(lspInitOpsPath) && File.Exists(lspInitOpsPath)) + { + try + { + string overrideString = File.ReadAllText(lspInitOpsPath); + if (!string.IsNullOrWhiteSpace(overrideString)) + { + localInitOps = JObject.Parse(overrideString); + } + } + catch (IOException ex) + { + RustAnalyzerPackage.JTF.RunAsync(async () => + { + await VsCommon.ShowErrorMessageAsync( + "Rust Analyzer", + $"Error reading JSON file from '{lspInitOpsPath}': {ex.Message}"); + }).FireAndForget(); + localInitOps = new JObject(); + } + catch (System.Exception ex) + { + RustAnalyzerPackage.JTF.RunAsync(async () => + { + await VsCommon.ShowErrorMessageAsync( + "Rust Analyzer", + $"Error parsing JSON from file '{lspInitOpsPath}': {ex.Message}"); + }).FireAndForget(); + localInitOps = new JObject(); + } + } + + try + { + globalInitOps.Merge(localInitOps, new JsonMergeSettings + { + MergeArrayHandling = MergeArrayHandling.Replace, + MergeNullValueHandling = MergeNullValueHandling.Merge + }); + } + catch (System.Exception ex) + { + RustAnalyzerPackage.JTF.RunAsync(async () => + { + await VsCommon.ShowErrorMessageAsync( + "Rust Analyzer", + $"Error merging LSP initialization options: {ex.Message}"); + }).FireAndForget(); + return new JObject(); + } + + return globalInitOps; + } + + [Browsable(true)] + [Category(SettingsInfo.KindConfig)] + [DisplayName("Environment variables")] + [Description("Environment variables passed to rust-analyzer.exe, example:'''\nRA_LOG=info Env2=Test Env3=Hello\n'''\n" + + "Reopening the Solution is required for changes to this setting to take effect.")] + public string RustAnalyzerEnvArguments { get; set; } = string.Empty; + + public JObject GetRustAnalyzerEnvArguments() + { + var envArgs = new JObject(); + foreach (var arg in RustAnalyzerEnvArguments.Split(new[] { ' ' }, System.StringSplitOptions.RemoveEmptyEntries)) + { + var kvp = arg.Split(new[] { '=' }, 2); + + // Set Env inputs without = to empty string + var val = string.Empty; + if (kvp.Length == 2) + { + val = kvp[1]; + } + + envArgs[kvp[0]] = val; + } + + return envArgs; + } + + [Browsable(true)] + [Category(SettingsInfo.KindConfig)] + [DisplayName("Enable Rust Analyzer Stderr Logging")] + [Description("If enabled, output from rust-analyzer will be logged to the Output window.\n" + + "Reopening the Solution is required for changes to this setting to take effect.")] + public bool EnableRustAnalyzerStderrLogging { get; set; } = false; + } diff --git a/src/RustAnalyzer/Infrastructure/SettingsInfo.cs b/src/RustAnalyzer/Infrastructure/SettingsInfo.cs index cf6d20c..8bc8f7f 100644 --- a/src/RustAnalyzer/Infrastructure/SettingsInfo.cs +++ b/src/RustAnalyzer/Infrastructure/SettingsInfo.cs @@ -9,6 +9,7 @@ public class SettingsInfo public const string KindDebugger = "Debugger"; public const string KindBuild = "Build"; public const string KindTest = "Test"; + public const string KindConfig = "Rust Analyzer Config"; public const string TypeCommandLineArguments = nameof(NodeBrowseObject.CommandLineArguments); public const string TypeDebuggerEnvironment = nameof(NodeBrowseObject.DebuggerEnvironment); public const string TypeDebuggerWorkingDirectory = nameof(NodeBrowseObject.WorkingDirectory); diff --git a/src/RustAnalyzer/Infrastructure/VsCommon.cs b/src/RustAnalyzer/Infrastructure/VsCommon.cs index a086efd..d060f4a 100644 --- a/src/RustAnalyzer/Infrastructure/VsCommon.cs +++ b/src/RustAnalyzer/Infrastructure/VsCommon.cs @@ -58,6 +58,14 @@ public static async Task ShowInfoBarAsync(bool success, string message) await infoBar.TryShowInfoBarUIAsync(); } + public static async Task ShowErrorMessageAsync(string title, string message) + { + await RustAnalyzerPackage.JTF.SwitchToMainThreadAsync(); + await CommunityVS.MessageBox.ShowErrorAsync( + title.AddPrefixToMessage(), + message); + } + public static string GetFullName(this VSITEMSELECTION item) { ThreadHelper.ThrowIfNotOnUIThread(); diff --git a/src/RustAnalyzer/LanguageService/LanguageClient.cs b/src/RustAnalyzer/LanguageService/LanguageClient.cs index 4db3c1f..cdebb5f 100644 --- a/src/RustAnalyzer/LanguageService/LanguageClient.cs +++ b/src/RustAnalyzer/LanguageService/LanguageClient.cs @@ -50,7 +50,7 @@ public IEnumerable ConfigurationSections } } - public object InitializationOptions => null; + public object InitializationOptions { get; set; } = null; public IEnumerable FilesToWatch => null; @@ -62,6 +62,7 @@ public IEnumerable ConfigurationSections public async Task ActivateAsync(CancellationToken token) { + var options = await Options.GetLiveInstanceAsync(); var rlsPath = await RADownloader.GetExePathAsync(); L.WriteLine("Starting rust-analyzer from path: {0}.", rlsPath); ProcessStartInfo info = new() @@ -69,19 +70,44 @@ public async Task ActivateAsync(CancellationToken token) FileName = rlsPath, RedirectStandardInput = true, RedirectStandardOutput = true, + RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Minimized, WorkingDirectory = WorkspaceService.CurrentWorkspace?.Location ?? Path.GetDirectoryName(rlsPath), }; + foreach (var prop in options.GetRustAnalyzerEnvArguments().Properties()) + { + // Value will never be null, see Options.cs::GetRustAnalyzerEnvArguments implementation + info.EnvironmentVariables[prop.Name] = (string)prop.Value!; + } + Process process = new() { StartInfo = info }; + string topDir = WorkspaceService.CurrentWorkspace?.Location; + var mergedOptions = options.GetMergedLspInitializationOptions(topDir); + + InitializationOptions = mergedOptions; + L.WriteLine("Parsing lsp initializationOptions: {0}", InitializationOptions.SerializeObject(Newtonsoft.Json.Formatting.Indented)); + if (process.Start()) { + if (options.EnableRustAnalyzerStderrLogging) + { + _ = Task.Run(async () => + { + string line; + while ((line = await process.StandardError.ReadLineAsync()) != null) + { + L.WriteLine("[rust-analyzer stderr] {0}", line); + } + }); + } + L.WriteLine("Done starting rust-analyzer from path. PID: {0}", process.Id); T.TrackEvent("rust-analyzer-start", ("Path", rlsPath));