diff --git a/src/StructuredLogViewer.Avalonia/Controls/BuildControl.xaml.cs b/src/StructuredLogViewer.Avalonia/Controls/BuildControl.xaml.cs
index 80a35391..e601345e 100644
--- a/src/StructuredLogViewer.Avalonia/Controls/BuildControl.xaml.cs
+++ b/src/StructuredLogViewer.Avalonia/Controls/BuildControl.xaml.cs
@@ -40,6 +40,7 @@ public partial class BuildControl : UserControl
private MenuItem copyNameItem;
private MenuItem copyValueItem;
private MenuItem viewSourceItem;
+ private MenuItem showFileInExplorerItem;
private MenuItem preprocessItem;
private MenuItem hideItem;
private ContextMenu sharedTreeContextMenu;
@@ -170,6 +171,7 @@ public BuildControl(Build build, string logFilePath)
copyNameItem = new MenuItem() { Header = "Copy name" };
copyValueItem = new MenuItem() { Header = "Copy value" };
viewSourceItem = new MenuItem() { Header = "View source" };
+ showFileInExplorerItem = new MenuItem() { Header = "Show in Explorer" };
preprocessItem = new MenuItem() { Header = "Preprocess" };
hideItem = new MenuItem() { Header = "Hide" };
copyItem.Click += (s, a) => Copy();
@@ -179,6 +181,7 @@ public BuildControl(Build build, string logFilePath)
copyNameItem.Click += (s, a) => CopyName();
copyValueItem.Click += (s, a) => CopyValue();
viewSourceItem.Click += (s, a) => Invoke(treeView.SelectedItem as BaseNode);
+ showFileInExplorerItem.Click += (s, a) => ShowFileInExplorer();
preprocessItem.Click += (s, a) => Preprocess(treeView.SelectedItem as IPreprocessable);
hideItem.Click += (s, a) => Delete();
contextMenu.AddItem(viewSourceItem);
@@ -189,6 +192,8 @@ public BuildControl(Build build, string logFilePath)
contextMenu.AddItem(sortChildrenByDurationItem);
contextMenu.AddItem(copyNameItem);
contextMenu.AddItem(copyValueItem);
+ contextMenu.AddItem(new Separator());
+ contextMenu.AddItem(showFileInExplorerItem);
contextMenu.AddItem(hideItem);
Style GetTreeViewItemStyle()
@@ -477,6 +482,7 @@ private void ContextMenu_Opened(object sender, RoutedEventArgs e)
copyNameItem.IsVisible = visibility;
copyValueItem.IsVisible = visibility;
viewSourceItem.IsVisible = CanView(node);
+ showFileInExplorerItem.IsVisible = CanShowInExplorer();
var hasChildren = node is TreeNode t && t.HasChildren;
copySubtreeItem.IsVisible = hasChildren;
sortChildrenByNameItem.IsVisible = hasChildren;
@@ -1107,6 +1113,21 @@ public void CopyValue()
}
}
+ public void ShowFileInExplorer()
+ {
+ string path = FileExplorerHelper.GetFilePathFromNode(treeView.SelectedItem as BaseNode);
+
+ if (path != null)
+ {
+ FileExplorerHelper.ShowInExplorer(path);
+ }
+ }
+
+ private bool CanShowInExplorer()
+ {
+ return FileExplorerHelper.GetFilePathFromNode(treeView.SelectedItem as BaseNode) is not null;
+ }
+
private void MoveSelectionOut(BaseNode node)
{
var parent = node.Parent;
diff --git a/src/StructuredLogViewer.Core/FileExplorerHelper.cs b/src/StructuredLogViewer.Core/FileExplorerHelper.cs
new file mode 100644
index 00000000..3a84c820
--- /dev/null
+++ b/src/StructuredLogViewer.Core/FileExplorerHelper.cs
@@ -0,0 +1,151 @@
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+using Microsoft.Build.Logging.StructuredLogger;
+
+#nullable enable
+
+namespace StructuredLogViewer
+{
+ ///
+ /// Helper class for showing files and directories in the system file explorer.
+ ///
+ public static class FileExplorerHelper
+ {
+ ///
+ /// Shows the specified file or directory in the system file explorer.
+ ///
+ /// The path to show in the file explorer.
+ public static void ShowInExplorer(string? path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ return;
+ }
+
+ try
+ {
+ if (File.Exists(path))
+ {
+ // Show file in file manager
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ Process.Start("explorer.exe", $"/select,\"{path}\"");
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ Process.Start("open", $"-R \"{path}\"");
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ // Try common Linux file managers
+ var directory = Path.GetDirectoryName(path);
+ if (Directory.Exists(directory))
+ {
+ Process.Start(new ProcessStartInfo(directory) { UseShellExecute = true });
+ }
+ }
+ }
+ else if (Directory.Exists(path))
+ {
+ // Open directory
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ Process.Start("explorer.exe", $"\"{path}\"");
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ Process.Start("open", $"\"{path}\"");
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
+ }
+ }
+ }
+ catch
+ {
+ // If that fails, try just opening the directory
+ try
+ {
+ var directory = File.Exists(path) ? Path.GetDirectoryName(path) : path;
+ if (Directory.Exists(directory))
+ {
+ Process.Start(new ProcessStartInfo(directory) { UseShellExecute = true });
+ }
+ }
+ catch
+ {
+ // Ignore any errors
+ }
+ }
+ }
+
+ ///
+ /// Gets a valid file path from the specified node if it contains one.
+ ///
+ /// The node to extract a file path from.
+ /// A valid file path if found, otherwise null.
+ public static string? GetFilePathFromNode(BaseNode? selectedNode)
+ {
+ if (selectedNode == null)
+ {
+ return null;
+ }
+
+ // Check for NameValueNode first
+ if (selectedNode is NameValueNode nameValueNode && IsValidExistingPath(nameValueNode.Value))
+ {
+ return nameValueNode.Value;
+ }
+
+ // Check for Item node (representing items in ItemGroups)
+ if (selectedNode is Item item && IsValidExistingPath(item.Text))
+ {
+ return item.Text;
+ }
+
+ // Check for file path in standard nodes
+ if (selectedNode is Import import && IsValidExistingPath(import.ImportedProjectFilePath))
+ {
+ return import.ImportedProjectFilePath;
+ }
+
+ if (selectedNode is IHasSourceFile file && IsValidExistingPath(file.SourceFilePath))
+ {
+ return file.SourceFilePath;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Checks if the specified path is a valid existing file or directory path.
+ ///
+ /// The path to validate.
+ /// True if the path is valid and exists, otherwise false.
+ public static bool IsValidExistingPath(string? path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return false;
+ }
+
+ try
+ {
+ // Check if it looks like a valid file path
+ if (path!.IndexOfAny(Path.GetInvalidPathChars()) != -1)
+ {
+ return false;
+ }
+
+ // Check if the file or directory actually exists
+ return File.Exists(path) || Directory.Exists(path);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/StructuredLogViewer/Controls/BuildControl.xaml.cs b/src/StructuredLogViewer/Controls/BuildControl.xaml.cs
index 1d34e621..8b35beb5 100644
--- a/src/StructuredLogViewer/Controls/BuildControl.xaml.cs
+++ b/src/StructuredLogViewer/Controls/BuildControl.xaml.cs
@@ -59,6 +59,7 @@ public partial class BuildControl : UserControl
private MenuItem viewFullTextItem;
private MenuItem openFileItem;
private MenuItem copyFilePathItem;
+ private MenuItem showFileInExplorerItem;
private MenuItem preprocessItem;
private MenuItem targetGraphItem;
private MenuItem nugetGraphItem;
@@ -226,6 +227,7 @@ public BuildControl(Build build, string logFilePath)
unfavoriteItem = new MenuItem() { Header = "Remove from Favorites" };
openFileItem = new MenuItem() { Header = "Open File" };
copyFilePathItem = new MenuItem() { Header = "Copy file path" };
+ showFileInExplorerItem = new MenuItem() { Header = "Show in Explorer" };
preprocessItem = new MenuItem() { Header = "Preprocess" };
targetGraphItem = new MenuItem { Header = "Target Graph" };
nugetGraphItem = new MenuItem { Header = "NuGet Graph" };
@@ -267,6 +269,7 @@ public BuildControl(Build build, string logFilePath)
unfavoriteItem.Click += (s, a) => RemoveFromFavorites();
openFileItem.Click += (s, a) => OpenFile();
copyFilePathItem.Click += (s, a) => CopyFilePath();
+ showFileInExplorerItem.Click += (s, a) => ShowFileInExplorer();
preprocessItem.Click += (s, a) => Preprocess(treeView.SelectedItem as IPreprocessable);
targetGraphItem.Click += (s, a) => ViewTargetGraph(treeView.SelectedItem as IProjectOrEvaluation);
nugetGraphItem.Click += (s, a) => ViewNuGetGraph(treeView.SelectedItem as IProjectOrEvaluation);
@@ -311,6 +314,9 @@ public BuildControl(Build build, string logFilePath)
contextMenu.AddItem(copyNameItem);
contextMenu.AddItem(copyValueItem);
+ contextMenu.AddItem(new Separator());
+ contextMenu.AddItem(showFileInExplorerItem);
+
contextMenu.AddItem(separator2);
contextMenu.AddItem(viewSubtreeTextItem);
@@ -967,6 +973,7 @@ private void ContextMenu_Opened(object sender, RoutedEventArgs e)
copyFilePathItem.Visibility = node is Import || (node is IHasSourceFile file && !string.IsNullOrEmpty(file.SourceFilePath))
? Visibility.Visible
: Visibility.Collapsed;
+ showFileInExplorerItem.Visibility = CanShowInExplorer() ? Visibility.Visible : Visibility.Collapsed;
var hasChildren = node is TreeNode t && t.HasChildren;
var hasChildrenVisibility = hasChildren ? Visibility.Visible : Visibility.Collapsed;
copySubtreeItem.Visibility = hasChildrenVisibility;
@@ -2054,6 +2061,21 @@ public void CopyFilePath()
}
}
+ public void ShowFileInExplorer()
+ {
+ string path = FileExplorerHelper.GetFilePathFromNode(treeView.SelectedItem as BaseNode);
+
+ if (path != null)
+ {
+ FileExplorerHelper.ShowInExplorer(path);
+ }
+ }
+
+ private bool CanShowInExplorer()
+ {
+ return FileExplorerHelper.GetFilePathFromNode(treeView.SelectedItem as BaseNode) is not null;
+ }
+
public void SearchInSubtree()
{
if (treeView.SelectedItem is TimedNode treeNode)