diff --git a/src/StructuredLogViewer.Core/SettingsService.cs b/src/StructuredLogViewer.Core/SettingsService.cs index 20bb2a92..d860b911 100644 --- a/src/StructuredLogViewer.Core/SettingsService.cs +++ b/src/StructuredLogViewer.Core/SettingsService.cs @@ -382,6 +382,14 @@ public static bool VSCodeHintDismissed set => Set(ref vsCodeHintDismissed, value); } + private static string? preferredVSCodeVariant; + public static string? PreferredVSCodeVariant + { + get => Get(ref preferredVSCodeVariant); + + set => Set(ref preferredVSCodeVariant, value); + } + private static string? windowPosition; public static string? WindowPosition { @@ -412,6 +420,7 @@ private static void EnsureSettingsRead() const string ShowConfigurationAndPlatformSetting = "ShowConfigurationAndPlatform="; const string UseDarkThemeSetting = "UseDarkTheme="; const string VSCodeHintDismissedSetting = "VSCodeHintDismissed="; + const string PreferredVSCodeVariantSetting = "PreferredVSCodeVariant="; const string WindowPositionSetting = "WindowPosition="; const string IgnoreEmbeddedFilesSetting = "IgnoreEmbeddedFiles="; @@ -424,6 +433,7 @@ private static void SaveSettings() sb.AppendLine(ShowConfigurationAndPlatformSetting + ShowConfigurationAndPlatform.ToString()); sb.AppendLine(UseDarkThemeSetting + useDarkTheme.ToString()); sb.AppendLine(VSCodeHintDismissedSetting + vsCodeHintDismissed.ToString()); + sb.AppendLine(PreferredVSCodeVariantSetting + preferredVSCodeVariant); sb.AppendLine(WindowPositionSetting + windowPosition); sb.AppendLine(IgnoreEmbeddedFilesSetting + IgnoreEmbeddedFiles); @@ -453,6 +463,7 @@ private static void ReadSettings() ProcessLine(ShowConfigurationAndPlatformSetting, line, ref ProjectOrEvaluationHelper.ShowConfigurationAndPlatform); ProcessLine(UseDarkThemeSetting, line, ref useDarkTheme); ProcessLine(VSCodeHintDismissedSetting, line, ref vsCodeHintDismissed); + ProcessString(PreferredVSCodeVariantSetting, line, ref preferredVSCodeVariant); ProcessString(WindowPositionSetting, line, ref windowPosition); ProcessString(IgnoreEmbeddedFilesSetting, line, ref ignoreEmbeddedFiles); diff --git a/src/StructuredLogViewer/Controls/BuildControl.xaml.cs b/src/StructuredLogViewer/Controls/BuildControl.xaml.cs index c8e38903..0ff40791 100644 --- a/src/StructuredLogViewer/Controls/BuildControl.xaml.cs +++ b/src/StructuredLogViewer/Controls/BuildControl.xaml.cs @@ -23,6 +23,22 @@ namespace StructuredLogViewer.Controls { + public class VSCodeInstallation + { + public string Name { get; } + public string ExePath { get; } + public string UriScheme { get; } + public string CliName { get; } + + public VSCodeInstallation(string name, string exePath, string uriScheme, string cliName) + { + Name = name; + ExePath = exePath; + UriScheme = uriScheme; + CliName = cliName; + } + } + public partial class BuildControl : UserControl { public Build Build { get; set; } @@ -491,7 +507,7 @@ private static string FindRepoRoot(string startDir) /// Launches VS Code with the workspace folder and binlog URI handler. /// Auto-installs the binlog-analyzer extension if not already installed. /// - public void OpenInVSCode() + public void OpenInVSCode(VSCodeInstallation installation = null) { var binlogPath = Build?.LogFilePath; if (string.IsNullOrEmpty(binlogPath)) @@ -500,11 +516,16 @@ public void OpenInVSCode() return; } - var codeExe = FindVSCodeExecutable(); - if (codeExe == null) + if (installation == null) + { + var installations = FindVSCodeInstallations(); + installation = installations.FirstOrDefault(); + } + + if (installation == null) { MessageBox.Show( - "Could not find VS Code (Code.exe). Make sure VS Code is installed.", + "Could not find VS Code or VS Code Insiders.\nMake sure one of them is installed.", "Open in VS Code", MessageBoxButton.OK, MessageBoxImage.Error); @@ -513,12 +534,12 @@ public void OpenInVSCode() try { - TPLTask.Run(() => EnsureExtensionInstalled(codeExe)); + TPLTask.Run(() => EnsureExtensionInstalled(installation)); var folder = GetWorkspacePath(); - // Build the URI to trigger the extension's binlog loading - var uri = "vscode://dotutils.binlog-analyzer/open?path=" + Uri.EscapeDataString(binlogPath); + // Build the URI using the correct scheme for this variant + var uri = $"{installation.UriScheme}://dotutils.binlog-analyzer/open?path=" + Uri.EscapeDataString(binlogPath); foreach (var attached in attachedBinlogs) { uri += "&path=" + Uri.EscapeDataString(attached); @@ -526,6 +547,7 @@ public void OpenInVSCode() // Launch VS Code with folder, then send URI after a short delay. // Combining --new-window + --open-url in one call can cause VS Code to ignore the folder. + var codeExe = installation.ExePath; var folderArg = !string.IsNullOrEmpty(folder) ? $"\"{folder}\"" : ""; Process.Start(new ProcessStartInfo { FileName = codeExe, Arguments = $"--new-window {folderArg}".Trim(), UseShellExecute = true }); @@ -543,8 +565,8 @@ public void OpenInVSCode() catch (Exception ex) { MessageBox.Show( - $"Failed to open VS Code.\n\n{ex.Message}", - "Open in VS Code", + $"Failed to open {installation.Name}.\n\n{ex.Message}", + $"Open in {installation.Name}", MessageBoxButton.OK, MessageBoxImage.Error); } @@ -552,15 +574,15 @@ public void OpenInVSCode() private static readonly string ExtensionId = "dotutils.binlog-analyzer"; - private static void EnsureExtensionInstalled(string codeExe) + private static void EnsureExtensionInstalled(VSCodeInstallation installation) { try { - var codeDir = Path.GetDirectoryName(codeExe); - var codeCli = Path.Combine(codeDir, "bin", "code.cmd"); + var codeDir = Path.GetDirectoryName(installation.ExePath); + var codeCli = Path.Combine(codeDir, "bin", installation.CliName + ".cmd"); if (!File.Exists(codeCli)) { - codeCli = Path.Combine(codeDir, "bin", "code"); + codeCli = Path.Combine(codeDir, "bin", installation.CliName); } // Check if extension is already installed @@ -600,41 +622,64 @@ private static void EnsureExtensionInstalled(string codeExe) } } - private static string FindVSCodeExecutable() + public static List FindVSCodeInstallations() { - // Check common install locations for Code.exe - string[] candidates = + var installations = new List(); + + var variants = new[] { - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "Microsoft VS Code", "Code.exe"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Microsoft VS Code", "Code.exe"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Microsoft VS Code", "Code.exe"), + new { Name = "VS Code", FolderName = "Microsoft VS Code", ExeName = "Code.exe", UriScheme = "vscode", CliName = "code" }, + new { Name = "VS Code Insiders", FolderName = "Microsoft VS Code Insiders", ExeName = "Code - Insiders.exe", UriScheme = "vscode-insiders", CliName = "code-insiders" }, }; - foreach (var candidate in candidates) + foreach (var variant in variants) { - if (File.Exists(candidate)) - return candidate; + string[] candidates = + { + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", variant.FolderName, variant.ExeName), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), variant.FolderName, variant.ExeName), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), variant.FolderName, variant.ExeName), + }; + + foreach (var candidate in candidates) + { + if (File.Exists(candidate)) + { + installations.Add(new VSCodeInstallation(variant.Name, candidate, variant.UriScheme, variant.CliName)); + break; + } + } } - // Fallback: resolve from code.cmd in PATH + // Fallback: resolve from code.cmd / code-insiders.cmd in PATH try { var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? Array.Empty(); - foreach (var dir in pathDirs) + foreach (var variant in variants) { - var codeCmdPath = Path.Combine(dir, "code.cmd"); - if (File.Exists(codeCmdPath)) + if (installations.Any(i => i.CliName == variant.CliName)) + continue; + + var cmdName = variant.CliName + ".cmd"; + foreach (var dir in pathDirs) { - // code.cmd is in /bin/, Code.exe is in / - var codeExe = Path.Combine(Path.GetDirectoryName(dir) ?? dir, "Code.exe"); - if (File.Exists(codeExe)) - return codeExe; + var codeCmdPath = Path.Combine(dir, cmdName); + if (File.Exists(codeCmdPath)) + { + // code.cmd is in /bin/, Code.exe is in / + var codeExe = Path.Combine(Path.GetDirectoryName(dir) ?? dir, variant.ExeName); + if (File.Exists(codeExe)) + { + installations.Add(new VSCodeInstallation(variant.Name, codeExe, variant.UriScheme, variant.CliName)); + break; + } + } } } } catch { } - return null; + return installations; } public void Dispose() diff --git a/src/StructuredLogViewer/MainWindow.xaml b/src/StructuredLogViewer/MainWindow.xaml index cba8ccb5..35bb07ad 100644 --- a/src/StructuredLogViewer/MainWindow.xaml +++ b/src/StructuredLogViewer/MainWindow.xaml @@ -84,18 +84,31 @@ + diff --git a/src/StructuredLogViewer/MainWindow.xaml.cs b/src/StructuredLogViewer/MainWindow.xaml.cs index b64fa4fc..b42b7b5e 100644 --- a/src/StructuredLogViewer/MainWindow.xaml.cs +++ b/src/StructuredLogViewer/MainWindow.xaml.cs @@ -29,6 +29,9 @@ public partial class MainWindow : Window private string lastSearchText; private double scale = 1.0; + private List vsCodeInstallations; + private VSCodeInstallation selectedVSCodeInstallation; + public const string DefaultTitle = "MSBuild Structured Log Viewer"; public string VersionMessage { get; set; } = "Locally built version"; @@ -487,6 +490,7 @@ private void SetContent(object content) projectFilePath = null; currentBuild = null; openInVSCodeButton.Visibility = Visibility.Collapsed; + vsCodeDropdownButton.Visibility = Visibility.Collapsed; attachBinlogButton.Visibility = Visibility.Collapsed; } @@ -496,7 +500,15 @@ private void SetContent(object content) SaveAsMenu.Visibility = Visibility.Visible; RedactSecretsMenu.Visibility = Visibility.Visible; + if (vsCodeInstallations == null) + { + DetectVSCodeInstallations(); + } + openInVSCodeButton.Visibility = Visibility.Visible; + vsCodeDropdownButton.Visibility = vsCodeInstallations.Count > 1 + ? Visibility.Visible + : Visibility.Collapsed; attachBinlogButton.Visibility = Visibility.Visible; UpdateAttachmentLabel(); UpdateVSCodeTooltip(buildControl); @@ -518,6 +530,7 @@ private void SetContent(object content) SaveAsMenu.Visibility = Visibility.Collapsed; RedactSecretsMenu.Visibility = Visibility.Collapsed; openInVSCodeButton.Visibility = Visibility.Collapsed; + vsCodeDropdownButton.Visibility = Visibility.Collapsed; attachBinlogButton.Visibility = Visibility.Collapsed; vsCodeHintBar.Visibility = Visibility.Collapsed; } @@ -1278,7 +1291,7 @@ private void OpenInVSCode_Click(object sender, RoutedEventArgs e) var buildControl = CurrentBuildControl; if (buildControl != null) { - buildControl.OpenInVSCode(); + buildControl.OpenInVSCode(selectedVSCodeInstallation); } } @@ -1309,13 +1322,14 @@ private void UpdateAttachmentLabel() { var buildControl = CurrentBuildControl; int count = buildControl?.AttachedBinlogCount ?? 0; + var variantName = selectedVSCodeInstallation?.Name ?? "VS Code"; if (count > 0) { - openInVSCodeText.Text = $"Open in VS Code (+{count} binlog{(count > 1 ? "s" : "")})"; + openInVSCodeText.Text = $"Open in {variantName} (+{count} binlog{(count > 1 ? "s" : "")})"; } else { - openInVSCodeText.Text = "Open in VS Code"; + openInVSCodeText.Text = $"Open in {variantName}"; } } @@ -1358,6 +1372,60 @@ private void UpdateVSCodeTooltip(BuildControl buildControl) openInVSCodeButton.ToolTip = tip; } + private void DetectVSCodeInstallations() + { + vsCodeInstallations = BuildControl.FindVSCodeInstallations(); + + // Restore preferred variant from settings, or default to first detected + var preferred = SettingsService.PreferredVSCodeVariant; + selectedVSCodeInstallation = vsCodeInstallations.FirstOrDefault(i => i.Name == preferred) + ?? vsCodeInstallations.FirstOrDefault(); + + // Populate the dropdown context menu + vsCodeVariantMenu.Items.Clear(); + foreach (var installation in vsCodeInstallations) + { + var item = new MenuItem + { + Header = installation.Name, + IsChecked = installation == selectedVSCodeInstallation, + Tag = installation + }; + item.Click += VSCodeVariant_Selected; + vsCodeVariantMenu.Items.Add(item); + } + } + + private void VSCodeDropdown_Click(object sender, RoutedEventArgs e) + { + if (sender is Button button && button.ContextMenu != null) + { + button.ContextMenu.PlacementTarget = button; + button.ContextMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom; + button.ContextMenu.IsOpen = true; + } + } + + private void VSCodeVariant_Selected(object sender, RoutedEventArgs e) + { + if (sender is MenuItem menuItem && menuItem.Tag is VSCodeInstallation installation) + { + selectedVSCodeInstallation = installation; + SettingsService.PreferredVSCodeVariant = installation.Name; + + // Update checkmarks + foreach (var item in vsCodeVariantMenu.Items) + { + if (item is MenuItem mi) + { + mi.IsChecked = mi.Tag == installation; + } + } + + UpdateAttachmentLabel(); + } + } + private void DismissVSCodeHint_Click(object sender, RoutedEventArgs e) { vsCodeHintBar.Visibility = Visibility.Collapsed;