diff --git a/src/StructuredLogViewer.Core/SettingsService.cs b/src/StructuredLogViewer.Core/SettingsService.cs index 1565d742..20bb2a92 100644 --- a/src/StructuredLogViewer.Core/SettingsService.cs +++ b/src/StructuredLogViewer.Core/SettingsService.cs @@ -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 { @@ -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="; @@ -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); @@ -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); diff --git a/src/StructuredLogViewer/Controls/BuildControl.xaml.cs b/src/StructuredLogViewer/Controls/BuildControl.xaml.cs index 6ff967a1..c8e38903 100644 --- a/src/StructuredLogViewer/Controls/BuildControl.xaml.cs +++ b/src/StructuredLogViewer/Controls/BuildControl.xaml.cs @@ -29,6 +29,17 @@ public partial class BuildControl : UserControl public TreeViewItem SelectedTreeViewItem { get; private set; } public string LogFilePath => Build?.LogFilePath; + private readonly List attachedBinlogs = new List(); + 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; @@ -440,6 +451,192 @@ on the node will navigate to the corresponding source code associated with the n centralTabControl.SelectionChanged += CentralTabControl_SelectionChanged; } + /// + /// Returns the workspace directory for VS Code. + /// Uses the binlog file's directory, or null if the path doesn't exist locally. + /// + 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; + } + + /// + /// Walks up from a directory to find a repository root (contains .git, .sln, or .csproj). + /// + 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; + } + + /// + /// Launches VS Code with the workspace folder and binlog URI handler. + /// Auto-installs the binlog-analyzer extension if not already installed. + /// + 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(); + foreach (var dir in pathDirs) + { + var codeCmdPath = Path.Combine(dir, "code.cmd"); + if (File.Exists(codeCmdPath)) + { + // 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; + } + } + } + catch { } + + return null; + } + public void Dispose() { // WPF controls diff --git a/src/StructuredLogViewer/MainWindow.xaml b/src/StructuredLogViewer/MainWindow.xaml index b7812238..cba8ccb5 100644 --- a/src/StructuredLogViewer/MainWindow.xaml +++ b/src/StructuredLogViewer/MainWindow.xaml @@ -44,7 +44,6 @@ - @@ -72,22 +71,60 @@ MinWidth="16" MinHeight="16">⨯ - + + + + + +