From 06cb3ca5af1c7ece8e293d5658b37f50c9cf878c Mon Sep 17 00:00:00 2001 From: tpvdb Date: Mon, 22 Sep 2025 02:18:10 +0200 Subject: [PATCH 1/2] feat RA config & Env var parsing Implements settings based parsing of config for rust analyzer (https://rust-analyzer.github.io/book/configuration.html) Adds option to parse Rust Analyzer env vars Adds optional logging for rust analyzer stderr --- src/RustAnalyzer/Infrastructure/Options.cs | 133 ++++++++++++++++++ .../Infrastructure/SettingsInfo.cs | 1 + src/RustAnalyzer/Infrastructure/VsCommon.cs | 8 ++ .../LanguageService/LanguageClient.cs | 28 +++- 4 files changed, 169 insertions(+), 1 deletion(-) diff --git a/src/RustAnalyzer/Infrastructure/Options.cs b/src/RustAnalyzer/Infrastructure/Options.cs index efbf4ed..67d192c 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 @@ -84,4 +94,127 @@ public class Options : BaseOptionModel, ISettingsServiceDefaults [DisplayName("Execution environment")] [Description($"Additioanal 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("Rust analyzer lsp initialization initializationOptions")] + [Description("JSON object parsed to initializationOptions, see https://rust-analyzer.github.io/book/configuration\n" + + "you can overwrite this global setting by placing a file named 'lsp_initializationOptions.json' in your top level project directory" + + "the two files will then get merged using a deep merge strategy (see https://jsoncompare.com/json-merge-tool)\n" + + "!!Make shure the json you enter is escaped correctly (no\\ in Windows paths or sth)\n" + + "Restarting VS is required for changes to this settign (or the .json) 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("Rust analyzer environment variables")] + [Description("Environment variables passed to rust-analyzer.exe, example:\nRA_LOG=info Env2=Test Env3=Hello")] + 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.")] + 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)); From 1c98820fec90cd562991a09ae62b480dcecfd580 Mon Sep 17 00:00:00 2001 From: tpvdb Date: Mon, 22 Sep 2025 03:00:44 +0200 Subject: [PATCH 2/2] Fix typing --- src/RustAnalyzer/Infrastructure/Options.cs | 32 ++++++++++++---------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/RustAnalyzer/Infrastructure/Options.cs b/src/RustAnalyzer/Infrastructure/Options.cs index 67d192c..6ea0064 100644 --- a/src/RustAnalyzer/Infrastructure/Options.cs +++ b/src/RustAnalyzer/Infrastructure/Options.cs @@ -92,17 +92,17 @@ 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("Rust analyzer lsp initialization initializationOptions")] - [Description("JSON object parsed to initializationOptions, see https://rust-analyzer.github.io/book/configuration\n" + - "you can overwrite this global setting by placing a file named 'lsp_initializationOptions.json' in your top level project directory" + - "the two files will then get merged using a deep merge strategy (see https://jsoncompare.com/json-merge-tool)\n" + - "!!Make shure the json you enter is escaped correctly (no\\ in Windows paths or sth)\n" + - "Restarting VS is required for changes to this settign (or the .json) to take effect")] + [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; } = "{}"; @@ -121,7 +121,7 @@ public JObject GetMergedLspInitializationOptions(string projectRootDir) } catch (System.Exception ex) { - RustAnalyzerPackage.JTF.RunAsync(async () => + RustAnalyzerPackage.JTF.RunAsync(async () => { await VsCommon.ShowErrorMessageAsync( "Rust Analyzer", @@ -143,7 +143,7 @@ await VsCommon.ShowErrorMessageAsync( } catch (IOException ex) { - RustAnalyzerPackage.JTF.RunAsync(async () => + RustAnalyzerPackage.JTF.RunAsync(async () => { await VsCommon.ShowErrorMessageAsync( "Rust Analyzer", @@ -153,7 +153,7 @@ await VsCommon.ShowErrorMessageAsync( } catch (System.Exception ex) { - RustAnalyzerPackage.JTF.RunAsync(async () => + RustAnalyzerPackage.JTF.RunAsync(async () => { await VsCommon.ShowErrorMessageAsync( "Rust Analyzer", @@ -173,10 +173,10 @@ await VsCommon.ShowErrorMessageAsync( } catch (System.Exception ex) { - RustAnalyzerPackage.JTF.RunAsync(async () => + RustAnalyzerPackage.JTF.RunAsync(async () => { await VsCommon.ShowErrorMessageAsync( - "Rust Analyzer", + "Rust Analyzer", $"Error merging LSP initialization options: {ex.Message}"); }).FireAndForget(); return new JObject(); @@ -187,8 +187,9 @@ await VsCommon.ShowErrorMessageAsync( [Browsable(true)] [Category(SettingsInfo.KindConfig)] - [DisplayName("Rust analyzer environment variables")] - [Description("Environment variables passed to rust-analyzer.exe, example:\nRA_LOG=info Env2=Test Env3=Hello")] + [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() @@ -214,7 +215,8 @@ public JObject GetRustAnalyzerEnvArguments() [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.")] + [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; }