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 @@ -374,6 +374,14 @@ public static bool UseDarkTheme
set => Set(ref useDarkTheme, value);
}

private static bool vsCodeHintDismissed = false;
public static bool VSCodeHintDismissed
{
get => Get(ref vsCodeHintDismissed);

set => Set(ref vsCodeHintDismissed, value);
}

private static string? windowPosition;
public static string? WindowPosition
{
Expand Down Expand Up @@ -403,6 +411,7 @@ private static void EnsureSettingsRead()
const string MarkResultsInTreeSetting = "MarkResultsInTree=";
const string ShowConfigurationAndPlatformSetting = "ShowConfigurationAndPlatform=";
const string UseDarkThemeSetting = "UseDarkTheme=";
const string VSCodeHintDismissedSetting = "VSCodeHintDismissed=";
const string WindowPositionSetting = "WindowPosition=";
const string IgnoreEmbeddedFilesSetting = "IgnoreEmbeddedFiles=";

Expand All @@ -414,6 +423,7 @@ private static void SaveSettings()
sb.AppendLine(MarkResultsInTreeSetting + markResultsInTree.ToString());
sb.AppendLine(ShowConfigurationAndPlatformSetting + ShowConfigurationAndPlatform.ToString());
sb.AppendLine(UseDarkThemeSetting + useDarkTheme.ToString());
sb.AppendLine(VSCodeHintDismissedSetting + vsCodeHintDismissed.ToString());
sb.AppendLine(WindowPositionSetting + windowPosition);
sb.AppendLine(IgnoreEmbeddedFilesSetting + IgnoreEmbeddedFiles);

Expand Down Expand Up @@ -442,6 +452,7 @@ private static void ReadSettings()
ProcessLine(MarkResultsInTreeSetting, line, ref markResultsInTree);
ProcessLine(ShowConfigurationAndPlatformSetting, line, ref ProjectOrEvaluationHelper.ShowConfigurationAndPlatform);
ProcessLine(UseDarkThemeSetting, line, ref useDarkTheme);
ProcessLine(VSCodeHintDismissedSetting, line, ref vsCodeHintDismissed);
ProcessString(WindowPositionSetting, line, ref windowPosition);
ProcessString(IgnoreEmbeddedFilesSetting, line, ref ignoreEmbeddedFiles);

Expand Down
197 changes: 197 additions & 0 deletions src/StructuredLogViewer/Controls/BuildControl.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ public partial class BuildControl : UserControl
public TreeViewItem SelectedTreeViewItem { get; private set; }
public string LogFilePath => Build?.LogFilePath;

private readonly List<string> attachedBinlogs = new List<string>();
public int AttachedBinlogCount => attachedBinlogs.Count;

public void AttachBinlog(string path)
{
if (!string.IsNullOrEmpty(path) && !attachedBinlogs.Contains(path, StringComparer.OrdinalIgnoreCase))
{
attachedBinlogs.Add(path);
}
}

private SourceFileResolver sourceFileResolver;
private ArchiveFileResolver archiveFile => sourceFileResolver.ArchiveFile;
private PreprocessedFileManager preprocessedFileManager;
Expand Down Expand Up @@ -440,6 +451,192 @@ on the node will navigate to the corresponding source code associated with the n
centralTabControl.SelectionChanged += CentralTabControl_SelectionChanged;
}

/// <summary>
/// Returns the workspace directory for VS Code.
/// Uses the binlog file's directory, or null if the path doesn't exist locally.
/// </summary>
public string GetWorkspacePath()
{
if (Build == null) return null;

var binlogDir = Path.GetDirectoryName(Build.LogFilePath);
if (!string.IsNullOrEmpty(binlogDir) && Directory.Exists(binlogDir))
{
return FindRepoRoot(binlogDir) ?? binlogDir;
}

return null;
}

/// <summary>
/// Walks up from a directory to find a repository root (contains .git, .sln, or .csproj).
/// </summary>
private static string FindRepoRoot(string startDir)
{
var dir = startDir;
while (!string.IsNullOrEmpty(dir))
{
if (Directory.Exists(Path.Combine(dir, ".git")))
return dir;
if (Directory.GetFiles(dir, "*.sln", SearchOption.TopDirectoryOnly).Length > 0)
return dir;
var parent = Path.GetDirectoryName(dir);
if (parent == dir) break;
dir = parent;
}
return null;
}

/// <summary>
/// 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()
{
var binlogPath = Build?.LogFilePath;
if (string.IsNullOrEmpty(binlogPath))
{
MessageBox.Show("No binlog file path available.", "Open in VS Code", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}

var codeExe = FindVSCodeExecutable();
if (codeExe == null)
{
MessageBox.Show(
"Could not find VS Code (Code.exe). Make sure VS Code is installed.",
"Open in VS Code",
MessageBoxButton.OK,
MessageBoxImage.Error);
return;
}

try
{
TPLTask.Run(() => EnsureExtensionInstalled(codeExe));

var folder = GetWorkspacePath();

// Build the URI to trigger the extension's binlog loading
var uri = "vscode://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 folderArg = !string.IsNullOrEmpty(folder) ? $"\"{folder}\"" : "";
Process.Start(new ProcessStartInfo { FileName = codeExe, Arguments = $"--new-window {folderArg}".Trim(), UseShellExecute = true });

var capturedUri = uri;
TPLTask.Run(async () =>
{
try
{
await TPLTask.Delay(1000);
Process.Start(new ProcessStartInfo { FileName = codeExe, Arguments = $"--open-url \"{capturedUri}\"", UseShellExecute = true });
}
catch { }
});
}
catch (Exception ex)
{
MessageBox.Show(
$"Failed to open VS Code.\n\n{ex.Message}",
"Open in VS Code",
MessageBoxButton.OK,
MessageBoxImage.Error);
}
}

private static readonly string ExtensionId = "dotutils.binlog-analyzer";

private static void EnsureExtensionInstalled(string codeExe)
{
try
{
var codeDir = Path.GetDirectoryName(codeExe);
var codeCli = Path.Combine(codeDir, "bin", "code.cmd");
if (!File.Exists(codeCli))
{
codeCli = Path.Combine(codeDir, "bin", "code");
}

// Check if extension is already installed
var checkPsi = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c \"{codeCli}\" --list-extensions",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
};

using var checkProc = Process.Start(checkPsi);
var output = checkProc?.StandardOutput.ReadToEnd() ?? "";
checkProc?.WaitForExit(10000);

if (output.IndexOf(ExtensionId, StringComparison.OrdinalIgnoreCase) >= 0)
{
return;
}

// Install from VS Code Marketplace
var installPsi = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c \"{codeCli}\" --install-extension {ExtensionId} --force",
UseShellExecute = false,
CreateNoWindow = true,
};

using var installProc = Process.Start(installPsi);
installProc?.WaitForExit(60000);
}
catch
{
// Non-fatal — user can install manually
}
}

private static string FindVSCodeExecutable()
{
// Check common install locations for Code.exe
string[] candidates =
{
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"),
};

foreach (var candidate in candidates)
{
if (File.Exists(candidate))
return candidate;
}

// Fallback: resolve from code.cmd in PATH
try
{
var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? Array.Empty<string>();
foreach (var dir in pathDirs)
{
var codeCmdPath = Path.Combine(dir, "code.cmd");
if (File.Exists(codeCmdPath))
{
// 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;
}
}
}
catch { }

return null;
}

public void Dispose()
{
// WPF controls
Expand Down
59 changes: 48 additions & 11 deletions src/StructuredLogViewer/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
<MenuItem Header="Search Syntax" Click="HelpLink3_Click" />
<MenuItem Header="https://github.com/KirillOsenkov/MSBuildStructuredLog" Click="HelpLink_Click" />
<MenuItem Header="https://msbuildlog.com" Click="HelpLink2_Click" />
<MenuItem Header="Try a prototype with LLM-integration" Click="LLMButton_Click" />
<Separator />
<MenuItem Header="About" Click="HelpAbout_Click" />
</MenuItem>
Expand Down Expand Up @@ -72,22 +71,60 @@
MinWidth="16"
MinHeight="16">⨯</Button>
</StackPanel>
<Button x:Name="llmButton"
<Button x:Name="attachBinlogButton"
Padding="6,2,6,2"
Margin="0,0,2,0"
Click="AttachBinlog_Click"
ToolTip="Attach additional binlog files to send to VS Code for comparison"
Background="{DynamicResource Theme_InfoBarBackground}"
Foreground="{DynamicResource Theme_InfoBarForeground}"
BorderThickness="1"
Visibility="Collapsed">
<TextBlock Text="📎" FontSize="14" />
</Button>
<Button x:Name="openInVSCodeButton"
Padding="8,2,8,2"
Margin="0,0,8,0"
Click="LLMButton_Click"
VerticalAlignment="Center"
ToolTip="Try a prototype with LLM integration"
Click="OpenInVSCode_Click"
ToolTip="Open in VS Code with Copilot Chat and binlog MCP tools"
Background="{DynamicResource Theme_InfoBarBackground}"
Foreground="{DynamicResource Theme_Foreground}"
BorderThickness="1">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="✨" FontSize="14" Margin="0,0,4,0" VerticalAlignment="Center" />
<TextBlock Text="LLM integration (Preview)" VerticalAlignment="Center" />
Foreground="{DynamicResource Theme_InfoBarForeground}"
BorderThickness="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>
</StackPanel>
</Grid>
<!-- VS Code hint bar -->
<Border x:Name="vsCodeHintBar"
DockPanel.Dock="Top"
Background="{DynamicResource Theme_InfoBarBackground}"
BorderBrush="Gray"
BorderThickness="0,0,0,1"
Padding="10,6"
Visibility="Collapsed">
<DockPanel>
<Button DockPanel.Dock="Right"
Content="✕"
Background="Transparent"
BorderThickness="0"
Padding="4,0"
Click="DismissVSCodeHint_Click"
Cursor="Hand"
FontSize="12"
VerticalAlignment="Center"/>
<TextBlock VerticalAlignment="Center"
TextWrapping="Wrap">
<Run FontWeight="Bold">💡 Tip:</Run>
<Run> Click</Run>
<Run FontWeight="Bold">✨ Open in VS Code</Run>
<Run> to analyze with Copilot Chat. Use 📎 to attach extra binlogs.</Run>
</TextBlock>
</DockPanel>
</Border>
<Grid>
<ContentPresenter x:Name="mainContent" Margin="7"/>
</Grid>
Expand Down
Loading