Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 136 additions & 1 deletion src/RustAnalyzer/Infrastructure/Options.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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<Options>, ISettingsServiceDefaults
Expand Down Expand Up @@ -82,6 +92,131 @@ public class Options : BaseOptionModel<Options>, 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;

}
1 change: 1 addition & 0 deletions src/RustAnalyzer/Infrastructure/SettingsInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions src/RustAnalyzer/Infrastructure/VsCommon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
28 changes: 27 additions & 1 deletion src/RustAnalyzer/LanguageService/LanguageClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public IEnumerable<string> ConfigurationSections
}
}

public object InitializationOptions => null;
public object InitializationOptions { get; set; } = null;

public IEnumerable<string> FilesToWatch => null;

Expand All @@ -62,26 +62,52 @@ public IEnumerable<string> ConfigurationSections

public async Task<Connection> 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()
{
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));

Expand Down