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)