Skip to content
Merged
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
11 changes: 11 additions & 0 deletions src/StructuredLogViewer.Core/SettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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=";

Expand All @@ -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);

Expand Down Expand Up @@ -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);

Expand Down
107 changes: 76 additions & 31 deletions src/StructuredLogViewer/Controls/BuildControl.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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.
/// </summary>
public void OpenInVSCode()
public void OpenInVSCode(VSCodeInstallation installation = null)
{
var binlogPath = Build?.LogFilePath;
if (string.IsNullOrEmpty(binlogPath))
Expand All @@ -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);
Expand All @@ -513,19 +534,20 @@ 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);
}

// 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 });

Expand All @@ -543,24 +565,24 @@ 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);
}
}

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
Expand Down Expand Up @@ -600,41 +622,64 @@ private static void EnsureExtensionInstalled(string codeExe)
}
}

private static string FindVSCodeExecutable()
public static List<VSCodeInstallation> FindVSCodeInstallations()
{
// Check common install locations for Code.exe
string[] candidates =
var installations = new List<VSCodeInstallation>();

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<string>();
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 <install>/bin/, Code.exe is in <install>/
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 <install>/bin/, Code.exe is in <install>/
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()
Expand Down
17 changes: 15 additions & 2 deletions src/StructuredLogViewer/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,18 +84,31 @@
</Button>
<Button x:Name="openInVSCodeButton"
Padding="8,2,8,2"
Margin="0,0,8,0"
Click="OpenInVSCode_Click"
ToolTip="Open in VS Code with Copilot Chat and binlog MCP tools"
Background="{DynamicResource Theme_InfoBarBackground}"
Foreground="{DynamicResource Theme_InfoBarForeground}"
BorderThickness="1"
BorderThickness="1,1,0,1"
Visibility="Collapsed">
<StackPanel Orientation="Horizontal">
<TextBlock Text="✨" FontSize="14" Margin="0,0,4,0" />
<TextBlock x:Name="openInVSCodeText" Text="Open in VS Code" />
</StackPanel>
</Button>
<Button x:Name="vsCodeDropdownButton"
Padding="4,2,4,2"
Margin="0,0,8,0"
Click="VSCodeDropdown_Click"
ToolTip="Choose VS Code variant"
Background="{DynamicResource Theme_InfoBarBackground}"
Foreground="{DynamicResource Theme_InfoBarForeground}"
BorderThickness="1"
Visibility="Collapsed">
<TextBlock Text="▾" FontSize="10" />
<Button.ContextMenu>
<ContextMenu x:Name="vsCodeVariantMenu" />
</Button.ContextMenu>
</Button>
</StackPanel>
</Grid>
<!-- VS Code hint bar -->
Expand Down
74 changes: 71 additions & 3 deletions src/StructuredLogViewer/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ public partial class MainWindow : Window
private string lastSearchText;
private double scale = 1.0;

private List<VSCodeInstallation> vsCodeInstallations;
private VSCodeInstallation selectedVSCodeInstallation;

public const string DefaultTitle = "MSBuild Structured Log Viewer";

public string VersionMessage { get; set; } = "Locally built version";
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
Expand All @@ -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;
}
Expand Down Expand Up @@ -1278,7 +1291,7 @@ private void OpenInVSCode_Click(object sender, RoutedEventArgs e)
var buildControl = CurrentBuildControl;
if (buildControl != null)
{
buildControl.OpenInVSCode();
buildControl.OpenInVSCode(selectedVSCodeInstallation);
}
}

Expand Down Expand Up @@ -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}";
}
}

Expand Down Expand Up @@ -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;
Expand Down