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
21 changes: 21 additions & 0 deletions src/StructuredLogViewer.Avalonia/Controls/BuildControl.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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()
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
151 changes: 151 additions & 0 deletions src/StructuredLogViewer.Core/FileExplorerHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Build.Logging.StructuredLogger;

#nullable enable

namespace StructuredLogViewer
{
/// <summary>
/// Helper class for showing files and directories in the system file explorer.
/// </summary>
public static class FileExplorerHelper
{
/// <summary>
/// Shows the specified file or directory in the system file explorer.
/// </summary>
/// <param name="path">The path to show in the file explorer.</param>
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
}
}
}

/// <summary>
/// Gets a valid file path from the specified node if it contains one.
/// </summary>
/// <param name="selectedNode">The node to extract a file path from.</param>
/// <returns>A valid file path if found, otherwise null.</returns>
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;
}

/// <summary>
/// Checks if the specified path is a valid existing file or directory path.
/// </summary>
/// <param name="path">The path to validate.</param>
/// <returns>True if the path is valid and exists, otherwise false.</returns>
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;
}
}
}
}
22 changes: 22 additions & 0 deletions src/StructuredLogViewer/Controls/BuildControl.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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" };
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down