From 334fc34ea4857b9d1ac11ede087628cfb5417e10 Mon Sep 17 00:00:00 2001 From: Kirill Osenkov Date: Fri, 21 Apr 2023 18:12:52 -0700 Subject: [PATCH 01/23] Roll back PR #672 At least until we figure out the root cause of #673 --- .../Serialization/StringCache.cs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/StructuredLogger/Serialization/StringCache.cs b/src/StructuredLogger/Serialization/StringCache.cs index 63f797d4..8ca0ef19 100644 --- a/src/StructuredLogger/Serialization/StringCache.cs +++ b/src/StructuredLogger/Serialization/StringCache.cs @@ -1,11 +1,10 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; +using System.Collections.Generic; namespace Microsoft.Build.Logging.StructuredLogger { public class StringCache { - private ConcurrentDictionary deduplicationMap = new ConcurrentDictionary(); + private Dictionary deduplicationMap = new Dictionary(); public IEnumerable Instances { get; set; } @@ -60,12 +59,25 @@ public string Intern(string text) text = text.NormalizeLineBreaks(); } - return deduplicationMap.GetOrAdd(text, text); + lock (deduplicationMap) + { + if (deduplicationMap.TryGetValue(text, out string existing)) + { + return existing; + } + + deduplicationMap[text] = text; + } + + return text; } public bool Contains(string text) { - return deduplicationMap.ContainsKey(text); + lock (deduplicationMap) + { + return deduplicationMap.ContainsKey(text); + } } public IDictionary InternStringDictionary(IDictionary inputDictionary) From ce809432150cea754304d691a54b7105c24ea8fb Mon Sep 17 00:00:00 2001 From: Felix Huang Date: Thu, 27 Apr 2023 15:50:09 -0700 Subject: [PATCH 02/23] Fix GoToTracing to work with zoom. Fix BT+ to only apply for cl. --- src/StructuredLogViewer/Controls/TracingControl.xaml.cs | 7 ++++--- src/StructuredLogger/Analyzers/CppAnalyzer.cs | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/StructuredLogViewer/Controls/TracingControl.xaml.cs b/src/StructuredLogViewer/Controls/TracingControl.xaml.cs index cf54e371..7d5f997e 100644 --- a/src/StructuredLogViewer/Controls/TracingControl.xaml.cs +++ b/src/StructuredLogViewer/Controls/TracingControl.xaml.cs @@ -742,13 +742,14 @@ public void GoToTimedNode(TimedNode node) } } + // Clear highlight when selection failed. if (textblock == null && activeTextBlock != null) { if (highlight.Parent is Panel parent) { parent.Children.Remove(highlight); } - + activeTextBlock = null; scrollViewer.ScrollToVerticalOffset(0); scrollViewer.ScrollToHorizontalOffset(0); } @@ -1116,8 +1117,8 @@ private void DrawHorizontalLine(Point originPoint, Point destinationPoint) private void ScrollToElement(TextField hit) { Point p = GetTextBlockToOverlayGrid(hit); - horizontalOffset = p.X > 20 ? p.X - 20 : p.X; - verticalOffset = p.Y > 20 ? p.Y - 20 : p.Y; + horizontalOffset = (p.X > 20 ? p.X - 20 : p.X) * scaleTransform.ScaleX; + verticalOffset = (p.Y > 20 ? p.Y - 20 : p.Y) * scaleTransform.ScaleY; horizontalOffset = Math.Max(horizontalOffset, 0); verticalOffset = Math.Max(verticalOffset, 0); diff --git a/src/StructuredLogger/Analyzers/CppAnalyzer.cs b/src/StructuredLogger/Analyzers/CppAnalyzer.cs index ede8795d..04a88d9d 100644 --- a/src/StructuredLogger/Analyzers/CppAnalyzer.cs +++ b/src/StructuredLogger/Analyzers/CppAnalyzer.cs @@ -48,7 +48,7 @@ public CppAnalyzerNode(CppAnalyzer cppAnalyzer) // time(C:\Program Files (x86)\Microsoft Visual Studio\2019\Preview\VC\Tools\MSVC\14.24.28218\bin\Hostx86\x86\c1xx.dll)=0.83512s < 985096605139 - 985104956295 > BB [C:\Users\yuehuang\AppData\Local\Temp\123\main36.cpp] // time(C:\Program Files(x86)\Microsoft Visual Studio\2019\Preview\VC\Tools\MSVC\14.24.28218\bin\Hostx86\x86\c2.dll)=0.01935s < 985104875296 - 985105068765 > BB[C: \Users\yuehuang\AppData\Local\Temp\123\main47.cpp] - const string regexBTPlus = @"^time\(.*(c1xx\.dll|c2\.dll)\)=(?'msTime'([0-9]*\.[0-9]+|[0-9]+))s \< (?'startTime'[\d]*) - (?'endTime'[\d]*) \>\s*(BB)?\s*\[(?'filename'[^\]]*)\]$"; + const string regexBTPlus = @"^time\(.*(c1\.dll|c1xx\.dll|c2\.dll)\)=(?'msTime'([0-9]*\.[0-9]+|[0-9]+))s \< (?'startTime'[\d]*) - (?'endTime'[\d]*) \>\s*(BB)?\s*\[(?'filename'[^\]]*)\]$"; readonly Regex BTPlus = new Regex(regexBTPlus, RegexOptions.Multiline); // Lib: Final Total time = 0.00804s < 5881693617253 - 5881693697673 > PB: 143409152 [D:\test\ConsoleApplication2\x64\Debug\ConsoleApplication2.lib] @@ -129,7 +129,7 @@ public void AnalyzeTask(CppTask cppTask) // For this view, de-batch them into individual task units. if ((cppTask.Name == MultiToolTaskName || cppTask.Name == CLTaskName) && cppTask.HasChildren) { - bool usingBTTime = globalBtplus; + bool usingBTTime = (cppTask.Name == CLTaskName) && globalBtplus; Dictionary blocks = new(); DateTime taskStartTime = cppTask.StartTime; DateTime taskCleanUpTime = cppTask.EndTime; @@ -250,7 +250,8 @@ public void AnalyzeTask(CppTask cppTask) if (!usingBTTime && child is Property property) { - if (property.Name == Strings.CommandLineArguments && property.Value.Contains("/Bt+")) + if (property.Name == Strings.CommandLineArguments + && (property.Value.Contains("/Bt+") || (property.Value.IndexOf("cl.exe", StringComparison.OrdinalIgnoreCase) >= 0 && globalBtplus))) { usingBTTime = true; } From 9eeca1191b31618906017c1ecdcc438c9094aa8b Mon Sep 17 00:00:00 2001 From: Kirill Osenkov Date: Thu, 4 May 2023 14:38:20 -0700 Subject: [PATCH 03/23] Add a sample for reading records without building the tree --- .../CompilerInvocationTests.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/StructuredLogger.Tests/CompilerInvocationTests.cs b/src/StructuredLogger.Tests/CompilerInvocationTests.cs index 044d8f91..07506bc2 100644 --- a/src/StructuredLogger.Tests/CompilerInvocationTests.cs +++ b/src/StructuredLogger.Tests/CompilerInvocationTests.cs @@ -1,3 +1,4 @@ +using Microsoft.Build.Framework; using Microsoft.Build.Logging.StructuredLogger; using Xunit; @@ -14,5 +15,31 @@ public void Parse1(string arg, string expected) var result = CompilerInvocationsReader.TrimCompilerExeFromCommandLine(arg, CompilerInvocation.CSharp); Assert.Equal(expected, result); } + + //[Fact] + public void ReadRecordsTest() + { + var binlog = @"C:\temp\msbuild.binlog"; + var records = BinaryLog.ReadRecords(binlog); + string lastTask = null; + + foreach (var record in records) + { + if (record.Args is TaskStartedEventArgs taskStarted) + { + lastTask = taskStarted.TaskName; + } + else if (record.Args is TaskParameterEventArgs taskParameters) + { + if (lastTask == "Csc" && taskParameters.ItemType == "Compile") + { + foreach (ITaskItem item in taskParameters.Items) + { + var csFilePath = item.ItemSpec; + } + } + } + } + } } } \ No newline at end of file From 07030732e6f4f112477a93b77f62e4252e50a551 Mon Sep 17 00:00:00 2001 From: Kirill Osenkov Date: Fri, 5 May 2023 11:53:33 -0700 Subject: [PATCH 04/23] Ensure GetText never returns null --- src/StructuredLogViewer/Controls/BuildControl.xaml.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StructuredLogViewer/Controls/BuildControl.xaml.cs b/src/StructuredLogViewer/Controls/BuildControl.xaml.cs index 16993683..2b1e01d8 100644 --- a/src/StructuredLogViewer/Controls/BuildControl.xaml.cs +++ b/src/StructuredLogViewer/Controls/BuildControl.xaml.cs @@ -1223,11 +1223,11 @@ string GetText(BaseNode node) { if (node is IHasTitle hasTitle) { - return hasTitle.Title; + return hasTitle.Title ?? ""; } else { - return node.ToString(); + return node.ToString() ?? ""; } } } From 4e3d4516087c84054f59d469246335aa773ef602 Mon Sep 17 00:00:00 2001 From: Kirill Osenkov Date: Mon, 8 May 2023 13:27:58 -0700 Subject: [PATCH 05/23] Bump MSBuild to 17.5.0 --- Directory.Build.props | 3 ++- src/StructuredLogger.Tests/StructuredLogger.Tests.csproj | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 2f226696..bfbfaa46 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -17,8 +17,9 @@ $(MSBuildThisFileDirectory)\key.snk embedded latest + $(NoWarn);NU1701;CS8632 - 16.10.0 + 17.5.0 3.5.107 diff --git a/src/StructuredLogger.Tests/StructuredLogger.Tests.csproj b/src/StructuredLogger.Tests/StructuredLogger.Tests.csproj index f13afd00..b5aa3af9 100644 --- a/src/StructuredLogger.Tests/StructuredLogger.Tests.csproj +++ b/src/StructuredLogger.Tests/StructuredLogger.Tests.csproj @@ -10,17 +10,16 @@ - + - - + + - From f19fe1e13e3a97e6022d9c1b31387652ea19056e Mon Sep 17 00:00:00 2001 From: Kirill Osenkov Date: Mon, 15 May 2023 14:14:25 -0700 Subject: [PATCH 06/23] Add a try/catch to ignore unsupported file paths Fixes #679 --- .../Analyzers/DoubleWritesAnalyzer.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/StructuredLogger/Analyzers/DoubleWritesAnalyzer.cs b/src/StructuredLogger/Analyzers/DoubleWritesAnalyzer.cs index 92abcdcd..8126aae6 100644 --- a/src/StructuredLogger/Analyzers/DoubleWritesAnalyzer.cs +++ b/src/StructuredLogger/Analyzers/DoubleWritesAnalyzer.cs @@ -108,8 +108,7 @@ private static bool IsDoubleWrite(KeyValuePair> bucket) } if (bucket.Value - .Select(f => new FileInfo(f)) - .Select(f => f.FullName) + .Select(f => GetFullPath(f)) .Distinct() .Count() == 1) { @@ -118,5 +117,19 @@ private static bool IsDoubleWrite(KeyValuePair> bucket) return true; } + + private static string GetFullPath(string filePath) + { + try + { + filePath = new FileInfo(filePath).FullName; + } + // https://github.com/KirillOsenkov/MSBuildStructuredLog/issues/679 + catch + { + } + + return filePath; + } } } From 260c84be7368d04ef989d2bc9157a068d2886d21 Mon Sep 17 00:00:00 2001 From: Kirill Osenkov Date: Fri, 19 May 2023 12:43:59 -0700 Subject: [PATCH 07/23] Target net7.0 for auxiliary projects --- src/BinlogTool/BinlogTool.csproj | 2 +- .../StructuredLogViewer.Avalonia.csproj | 4 ++-- src/StructuredLogViewer/StructuredLogViewer.csproj | 4 ++-- src/TaskRunner/TaskRunner.csproj | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/BinlogTool/BinlogTool.csproj b/src/BinlogTool/BinlogTool.csproj index 4ad029de..4946edf7 100644 --- a/src/BinlogTool/BinlogTool.csproj +++ b/src/BinlogTool/BinlogTool.csproj @@ -3,7 +3,7 @@ Exe 1.0.6 - net6.0 + net7.0 latest embedded false diff --git a/src/StructuredLogViewer.Avalonia/StructuredLogViewer.Avalonia.csproj b/src/StructuredLogViewer.Avalonia/StructuredLogViewer.Avalonia.csproj index 4218a93e..8ce93a03 100644 --- a/src/StructuredLogViewer.Avalonia/StructuredLogViewer.Avalonia.csproj +++ b/src/StructuredLogViewer.Avalonia/StructuredLogViewer.Avalonia.csproj @@ -1,8 +1,8 @@ - + WinExe - net6.0 + net7.0 win7-x64;ubuntu.14.04-x64;osx.10.12-x64;osx-arm64 StructuredLogger.ico diff --git a/src/StructuredLogViewer/StructuredLogViewer.csproj b/src/StructuredLogViewer/StructuredLogViewer.csproj index 691943bb..7d95db71 100644 --- a/src/StructuredLogViewer/StructuredLogViewer.csproj +++ b/src/StructuredLogViewer/StructuredLogViewer.csproj @@ -1,4 +1,4 @@ - + net472;net6.0-windows @@ -46,7 +46,7 @@ - $([System.IO.Path]::GetFullPath(`$(OutDir)\..\..\..\TaskRunner\$(Configuration)\net6.0`))\ + $([System.IO.Path]::GetFullPath(`$(OutDir)\..\..\..\TaskRunner\$(Configuration)\net7.0`))\ diff --git a/src/TaskRunner/TaskRunner.csproj b/src/TaskRunner/TaskRunner.csproj index b062f271..2912b7d1 100644 --- a/src/TaskRunner/TaskRunner.csproj +++ b/src/TaskRunner/TaskRunner.csproj @@ -2,14 +2,14 @@ Exe - net472;net6.0 + net472;net7.0 true - + false LatestMajor false From 49d1c91a417dcb41f965b53075fba4a5f8c4b113 Mon Sep 17 00:00:00 2001 From: Kirill Osenkov Date: Fri, 19 May 2023 13:56:06 -0700 Subject: [PATCH 08/23] Target net7 in a couple more places --- PublishNativeAOT.sh | 2 +- src/StructuredLogViewer/StructuredLogViewer.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PublishNativeAOT.sh b/PublishNativeAOT.sh index d4613ecf..e81d0193 100755 --- a/PublishNativeAOT.sh +++ b/PublishNativeAOT.sh @@ -1,3 +1,3 @@ #!/bin/sh dotnet publish -c Release -r linux-x64 -p:PublishNativeAot=True src/StructuredLogViewer.Avalonia/StructuredLogViewer.Avalonia.csproj -strip bin/StructuredLogViewer.Avalonia/Release/net6.0/linux-x64/publish/StructuredLogViewer.Avalonia \ No newline at end of file +strip bin/StructuredLogViewer.Avalonia/Release/net7.0/linux-x64/publish/StructuredLogViewer.Avalonia \ No newline at end of file diff --git a/src/StructuredLogViewer/StructuredLogViewer.csproj b/src/StructuredLogViewer/StructuredLogViewer.csproj index 7d95db71..fec0d473 100644 --- a/src/StructuredLogViewer/StructuredLogViewer.csproj +++ b/src/StructuredLogViewer/StructuredLogViewer.csproj @@ -1,7 +1,7 @@ - net472;net6.0-windows + net472;net7.0-windows WinExe true true From 780fd11500f98e03d01a32d6d1dc4c17ee74b3aa Mon Sep 17 00:00:00 2001 From: Andrii Kurdiumov Date: Sun, 21 May 2023 00:00:14 +0600 Subject: [PATCH 09/23] Bring Avalonia version closer to main app. I also did some formatting changes, so diff would point on the difference more cleanly. --- .../Controls/BuildControl.xaml | 207 ++++---- .../Controls/BuildControl.xaml.cs | 450 ++++++++++++++++-- .../Controls/DocumentWell.xaml | 68 +-- .../Controls/SearchAndResultsControl.xaml | 83 ++-- .../Controls/SearchAndResultsControl.xaml.cs | 67 ++- .../Controls/SplitterPanel.cs | 31 +- .../Controls/TextViewerControl.xaml | 120 ++--- src/StructuredLogViewer/AvaloniaExtensions.cs | 11 + .../Controls/BuildControl.xaml | 8 +- .../Controls/BuildControl.xaml.cs | 50 +- .../Controls/ImportLinkHighlighter.cs | 2 +- 11 files changed, 776 insertions(+), 321 deletions(-) create mode 100644 src/StructuredLogViewer/AvaloniaExtensions.cs diff --git a/src/StructuredLogViewer.Avalonia/Controls/BuildControl.xaml b/src/StructuredLogViewer.Avalonia/Controls/BuildControl.xaml index a81d273c..15c65a0b 100644 --- a/src/StructuredLogViewer.Avalonia/Controls/BuildControl.xaml +++ b/src/StructuredLogViewer.Avalonia/Controls/BuildControl.xaml @@ -1,119 +1,120 @@ - - + + + - - - - - - + + + + - - - - + + + + + + + + + + + - - - - - - + + + - - - - - - + + + + + + - - - + + + + + + - - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/src/StructuredLogViewer.Avalonia/Controls/BuildControl.xaml.cs b/src/StructuredLogViewer.Avalonia/Controls/BuildControl.xaml.cs index d4b467f6..92d15898 100644 --- a/src/StructuredLogViewer.Avalonia/Controls/BuildControl.xaml.cs +++ b/src/StructuredLogViewer.Avalonia/Controls/BuildControl.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.IO; @@ -17,6 +17,7 @@ using Avalonia.Styling; using Avalonia.Data; using Avalonia.Layout; +using System.Xml; namespace StructuredLogViewer.Avalonia.Controls { @@ -24,7 +25,7 @@ public partial class BuildControl : UserControl { public Build Build { get; set; } public TreeViewItem SelectedTreeViewItem { get; private set; } - public string LogFilePath { get; private set; } + public string LogFilePath => Build?.LogFilePath; private ScrollViewer scrollViewer; @@ -38,15 +39,17 @@ public partial class BuildControl : UserControl private MenuItem sortChildrenItem; private MenuItem copyNameItem; private MenuItem copyValueItem; - private MenuItem viewItem; + private MenuItem viewSourceItem; private MenuItem preprocessItem; private MenuItem hideItem; - private MenuItem copyAllItem; private ContextMenu sharedTreeContextMenu; + private ContextMenu filesTreeContextMenu; private TreeView treeView; private SearchAndResultsControl searchLogControl; private SearchAndResultsControl findInFilesControl; + private SearchAndResultsControl propertiesAndItemsControl; private TabItem filesTab; + private TabItem propertiesAndItemsTab; private TabItem findInFilesTab; private TreeView filesTree; private TabControl centralTabControl; @@ -54,9 +57,13 @@ public partial class BuildControl : UserControl private TabControl leftPaneTabControl; private TabItem searchLogTab; private DocumentWell documentWell; + private Border projectContextBorder; + private ContentControl propertiesAndItemsContext; public TreeView ActiveTreeView; + private PropertiesAndItemsSearch propertiesAndItemsSearch; + public BuildControl() { } @@ -71,7 +78,13 @@ public BuildControl(Build build, string logFilePath) searchLogControl.ExecuteSearch = (searchText, maxResults, cancellationToken) => { - var search = new Search(new[] { Build }, Build.StringTable.Instances, maxResults, SettingsService.MarkResultsInTree); + var search = new Search( + new[] { Build }, + Build.StringTable.Instances, + maxResults, + SettingsService.MarkResultsInTree + //, Build.StringTable // disable validation in production + ); var results = search.FindNodes(searchText, cancellationToken); return results; }; @@ -82,12 +95,37 @@ public BuildControl(Build build, string logFilePath) UpdateWatermark(); }; - findInFilesControl.ExecuteSearch = FindInFiles; - findInFilesControl.ResultsTreeBuilder = BuildFindResults; + propertiesAndItemsSearch = new PropertiesAndItemsSearch(); - Build = build; + propertiesAndItemsControl.ExecuteSearch = (searchText, maxResults, cancellationToken) => + { + var context = GetProjectContext() as TimedNode; + if (context == null) + { + return null; + } + + var results = propertiesAndItemsSearch.Search( + context, + searchText, + maxResults, + SettingsService.MarkResultsInTree, + cancellationToken); + + return results; + }; + propertiesAndItemsControl.ResultsTreeBuilder = BuildResultTree; + + UpdatePropertiesAndItemsWatermark(); + propertiesAndItemsControl.WatermarkDisplayed += () => + { + UpdatePropertiesAndItemsWatermark(); + }; + propertiesAndItemsControl.RecentItemsCategory = "PropertiesAndItems"; + + SetProjectContext(null); - LogFilePath = logFilePath; + Build = build; if (build.SourceFilesArchive != null) { @@ -100,11 +138,28 @@ public BuildControl(Build build, string logFilePath) sourceFileResolver = new SourceFileResolver(logFilePath); } + // Search Log | Properties and Items | Find in Files sharedTreeContextMenu = new ContextMenu(); - copyAllItem = new MenuItem() { Header = "Copy All" }; - copyAllItem.Click += (s, a) => CopyAll(); - sharedTreeContextMenu.AddItem(copyAllItem); - + var sharedCopyAllItem = new MenuItem() { Header = "Copy All" }; + var sharedCopySubtreeItem = new MenuItem() { Header = "Copy subtree" }; + sharedCopyAllItem.Click += (s, a) => CopyAll(); + sharedCopySubtreeItem.Click += (s, a) => CopySubtree(); + sharedTreeContextMenu.AddItem(sharedCopyAllItem); + sharedTreeContextMenu.AddItem(sharedCopySubtreeItem); + + // Files + filesTreeContextMenu = new ContextMenu(); + var filesCopyAllItem = new MenuItem { Header = "Copy All" }; + var filesCopyPathsItem = new MenuItem { Header = "Copy file paths" }; + var filesCopySubtreeItem = new MenuItem { Header = "Copy subtree" }; + filesCopyAllItem.Click += (s, a) => CopyAll(); + filesCopyPathsItem.Click += (s, a) => CopyPaths(); + filesCopySubtreeItem.Click += (s, a) => CopySubtree(); + filesTreeContextMenu.AddItem(filesCopyAllItem); + filesTreeContextMenu.AddItem(filesCopyPathsItem); + filesTreeContextMenu.AddItem(filesCopySubtreeItem); + + // Build Log var contextMenu = new ContextMenu(); // TODO //contextMenu.Opened += ContextMenu_Opened; @@ -113,18 +168,18 @@ public BuildControl(Build build, string logFilePath) sortChildrenItem = new MenuItem() { Header = "Sort children" }; copyNameItem = new MenuItem() { Header = "Copy name" }; copyValueItem = new MenuItem() { Header = "Copy value" }; - viewItem = new MenuItem() { Header = "View" }; + viewSourceItem = new MenuItem() { Header = "View source" }; preprocessItem = new MenuItem() { Header = "Preprocess" }; hideItem = new MenuItem() { Header = "Hide" }; copyItem.Click += (s, a) => Copy(); - copySubtreeItem.Click += (s, a) => CopySubtree(); + copySubtreeItem.Click += (s, a) => CopySubtree(treeView); sortChildrenItem.Click += (s, a) => SortChildren(); copyNameItem.Click += (s, a) => CopyName(); copyValueItem.Click += (s, a) => CopyValue(); - viewItem.Click += (s, a) => Invoke(treeView.SelectedItem as BaseNode); + viewSourceItem.Click += (s, a) => Invoke(treeView.SelectedItem as BaseNode); preprocessItem.Click += (s, a) => Preprocess(treeView.SelectedItem as IPreprocessable); hideItem.Click += (s, a) => Delete(); - contextMenu.AddItem(viewItem); + contextMenu.AddItem(viewSourceItem); contextMenu.AddItem(preprocessItem); contextMenu.AddItem(copyItem); contextMenu.AddItem(copySubtreeItem); @@ -165,6 +220,10 @@ Style GetTreeViewItemStyle() if (archiveFile != null) { + + findInFilesControl.ExecuteSearch = FindInFiles; + findInFilesControl.ResultsTreeBuilder = BuildFindResults; + filesTab.IsVisible = true; findInFilesTab.IsVisible = true; PopulateFilesTab(); @@ -180,6 +239,9 @@ on the node will navigate to the corresponding source code associated with the n More functionality is available from the right-click context menu for each node. Right-clicking a project node may show the 'Preprocess' option if the version of MSBuild was at least 15.3."; build.Unseal(); +#if DEBUG + text = build.StringTable.Intern(text); +#endif build.AddChild(new Note { Text = text }); build.Seal(); } @@ -188,11 +250,22 @@ on the node will navigate to the corresponding source code associated with the n TemplateApplied += BuildControl_Loaded; - preprocessedFileManager = new PreprocessedFileManager(Build, sourceFileResolver); - preprocessedFileManager.DisplayFile += path => DisplayFile(path); + preprocessedFileManager = new PreprocessedFileManager(this.Build, sourceFileResolver); + preprocessedFileManager.DisplayFile += filePath => DisplayFile(filePath); navigationHelper = new NavigationHelper(Build, sourceFileResolver); - navigationHelper.OpenFileRequested += path => DisplayFile(path); + navigationHelper.OpenFileRequested += filePath => DisplayFile(filePath); + + centralTabControl.SelectionChanged += CentralTabControl_SelectionChanged; + } + + private void CentralTabControl_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + var selectedItem = centralTabControl.SelectedItem as TabItem; + if (selectedItem == null) + { + return; + } } private void RegisterTreeViewHandlers(TreeView treeView) @@ -236,11 +309,15 @@ private void InitializeComponent() this.RegisterControl(out treeView, nameof(treeView)); this.RegisterControl(out filesTab, nameof(filesTab)); this.RegisterControl(out findInFilesTab, nameof(findInFilesTab)); + this.RegisterControl(out propertiesAndItemsTab, nameof(propertiesAndItemsTab)); + this.RegisterControl(out projectContextBorder, nameof(projectContextBorder)); + this.RegisterControl(out propertiesAndItemsContext, nameof(propertiesAndItemsContext)); this.RegisterControl(out filesTree, nameof(filesTree)); this.RegisterControl(out centralTabControl, nameof(centralTabControl)); this.RegisterControl(out breadCrumb, nameof(breadCrumb)); this.RegisterControl(out leftPaneTabControl, nameof(leftPaneTabControl)); this.RegisterControl(out searchLogTab, nameof(searchLogTab)); + this.RegisterControl(out propertiesAndItemsControl, nameof(propertiesAndItemsControl)); this.RegisterControl(out SplitterPanel tabs, nameof(tabs)); documentWell = tabs.SecondChild as DocumentWell; @@ -258,6 +335,7 @@ public void SelectTree() "Copying file from ", "Resolved file path is ", "There was a conflict", + "Encountered conflict between", "Building target completely ", "is newer than output ", "Property reassignment: $(", @@ -266,6 +344,7 @@ public void SelectTree() "out-of-date", "csc $task", "ResolveAssemblyReference $task", + "$task $time", "$message CompilerServer failed", "will be compiled because", }; @@ -375,18 +454,32 @@ private void UpdateWatermark() searchLogControl.WatermarkContent = new TextBlock { Text = text }; } - private void Preprocess(IPreprocessable preprocessable) + private void UpdatePropertiesAndItemsWatermark() { - preprocessedFileManager.ShowPreprocessed(preprocessable); + string watermarkText1 = $@"Look up properties or items for the selected project " + + "or a node under a project or evaluation. " + + "Properties and items might not be available for some projects.\n\n" + + "Surround the search term in quotes to find an exact match " + + "(turns off substring search). Prefix the search term with " + + "[[name=]] or [[value=]] to only search property and metadata names " + + "or values. Add [[$property ]], [[$item ]] or [[$metadata ]] to limit search " + + "to a specific node type."; + + var watermark = new TextBlock() { Text = watermarkText1 }; + + propertiesAndItemsControl.WatermarkContent = watermark; } + private void Preprocess(IPreprocessable project) => preprocessedFileManager.ShowPreprocessed(project); + + private void ContextMenu_Opened(object sender, RoutedEventArgs e) { var node = treeView.SelectedItem as BaseNode; var visibility = node is NameValueNode; copyNameItem.IsVisible = visibility; copyValueItem.IsVisible = visibility; - viewItem.IsVisible = CanView(node); + viewSourceItem.IsVisible = CanView(node); var hasChildren = node is TreeNode t && t.HasChildren; copySubtreeItem.IsVisible = hasChildren; sortChildrenItem.IsVisible = hasChildren; @@ -450,7 +543,10 @@ private IEnumerable BuildFindResults(object resultsObject, bool moreAvailable) if (!root.HasChildren && !string.IsNullOrEmpty(findInFilesControl.SearchText)) { - root.Children.Add(new Message { Text = "No results found." }); + root.Children.Add(new Message + { + Text = "No results found." + }); } return root.Children; @@ -477,6 +573,8 @@ private void PopulateFilesTab() }; sourceFile.AddChild(task); } + + sourceFile.SortChildren(); } foreach (var subFolder in root.Children.OfType()) @@ -528,6 +626,18 @@ private SourceFile AddSourceFile(Folder folder, string filePath, string[] parts, SourceFilePath = filePath, Name = parts[index] }; + + foreach (var target in GetTargets(filePath)) + { + file.AddChild(new Target + { + Name = target, + SourceFilePath = filePath + }); + } + + file.SortChildren(); + folder.AddChild(file); return file; } @@ -539,6 +649,56 @@ private SourceFile AddSourceFile(Folder folder, string filePath, string[] parts, } } + private IEnumerable GetTargets(string file) + { + if (file.EndsWith(".sln", StringComparison.OrdinalIgnoreCase) || + file.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + { + yield break; + } + + var content = sourceFileResolver.GetSourceFileText(file); + if (content == null) + { + yield break; + } + + var contentText = content.Text; + + if (!Utilities.LooksLikeXml(contentText)) + { + yield break; + } + + var doc = new XmlDocument(); + try + { + doc.LoadXml(contentText); + } + catch (Exception) + { + yield break; + } + + if (doc.DocumentElement == null) + { + yield break; + } + + var nsmgr = new XmlNamespaceManager(doc.NameTable); + nsmgr.AddNamespace("x", doc.DocumentElement.NamespaceURI); + var xmlNodeList = doc.SelectNodes(@"//x:Project/x:Target[@Name]", nsmgr); + if (xmlNodeList == null) + { + yield break; + } + + foreach (XmlNode selectNode in xmlNodeList) + { + yield return selectNode.Attributes["Name"].Value; + } + } + /// /// This is needed as a workaround for a weird bug. When the breadcrumb spans multiple lines /// and we click on an item on the first line, it truncates the breadcrumb up to that item. @@ -578,6 +738,7 @@ private void TreeView_SelectedItemChanged(object sender, AvaloniaPropertyChanged if (item != null) { UpdateBreadcrumb(item); + UpdateProjectContext(item); } } @@ -594,6 +755,59 @@ private void ResultsList_SelectionChanged(object sender, SelectionChangedEventAr } } + public void UpdateProjectContext(object item) + { + if (item is not BaseNode node) + { + return; + } + + var project = node.GetNearestParentOrSelf(); + if (project != null) + { + //projectEvaluation = Build.FindEvaluation(project.EvaluationId); + //if (projectEvaluation != null && (projectEvaluation.FindChild(Strings.Items) != null || projectEvaluation.FindChild(Strings.Properties) != null)) + //{ + // SetProjectContext(projectEvaluation); + // return; + //} + + //if (project.FindChild(Strings.Items) != null || project.FindChild(Strings.Properties) != null) + //{ + // SetProjectContext(project); + // return; + //} + + SetProjectContext(project); + return; + } + + var projectEvaluation = node.GetNearestParentOrSelf(); + if (projectEvaluation != null && (projectEvaluation.FindChild(Strings.Items) != null || projectEvaluation.FindChild(Strings.Properties) != null)) + { + SetProjectContext(projectEvaluation); + return; + } + + SetProjectContext(null); + } + + private object projectContext; + + public void SetProjectContext(object contents) + { + projectContext = contents; + propertiesAndItemsContext.Content = contents; + var visibility = contents != null; + projectContextBorder.IsVisible = visibility; + propertiesAndItemsControl.TopPanel.IsVisible = visibility; + } + + public IProjectOrEvaluation GetProjectContext() + { + return projectContext as IProjectOrEvaluation; + } + public void UpdateBreadcrumb(object item) { var node = item as BaseNode; @@ -642,10 +856,22 @@ private void BuildControl_Loaded(object sender, RoutedEventArgs e) treeView.Focus(); } - searchLogControl.SearchText = "$error"; + if (InitialSearchText == null) + { + InitialSearchText = "$error"; + } + } + + if (InitialSearchText != null) + { + searchLogControl.SearchText = InitialSearchText; } + + FocusSearch(); } + public string InitialSearchText { get; set; } + public void SelectItem(BaseNode item) { var parentChain = item.GetParentChainExcludingThis(); @@ -672,6 +898,71 @@ private void TreeView_KeyDown(object sender, KeyEventArgs args) CopySubtree(); args.Handled = true; } + else if (args.Key >= Key.A && args.Key <= Key.Z && args.KeyModifiers == KeyModifiers.None) + { + SelectItemByKey((char)('A' + args.Key - Key.A)); + args.Handled = true; + } + } + + private int characterMatchPrefixLength = 0; + + private void SelectItemByKey(char ch) + { + ch = char.ToLowerInvariant(ch); + + var selectedItem = treeView.SelectedItem as BaseNode; + if (selectedItem == null) + { + return; + } + + var parent = selectedItem.Parent; + if (parent == null) + { + return; + } + + var selectedText = GetText(selectedItem); + var prefix = selectedText.Substring(0, Math.Min(characterMatchPrefixLength, selectedText.Length)); + + var items = selectedItem.EnumerateSiblingsCycle(); + + search: + foreach (var item in items) + { + var text = GetText(item); + if (characterMatchPrefixLength < text.Length && text.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + var character = text[characterMatchPrefixLength]; + if (char.ToLowerInvariant(character) == ch) + { + characterMatchPrefixLength++; + SelectItem(item); + return; + } + } + } + + if (characterMatchPrefixLength > 0) + { + characterMatchPrefixLength = 0; + prefix = ""; + items = items.Skip(1).Concat(items.Take(1)); + goto search; + } + + string GetText(BaseNode node) + { + if (node is IHasTitle hasTitle) + { + return hasTitle.Title ?? ""; + } + else + { + return node.ToString() ?? ""; + } + } } public void FocusSearch() @@ -684,6 +975,15 @@ public void FocusSearch() { findInFilesControl.searchTextBox.Focus(); } + else if (leftPaneTabControl.SelectedItem == propertiesAndItemsTab) + { + propertiesAndItemsControl.searchTextBox.Focus(); + } + } + + public void SelectSearchTab() + { + leftPaneTabControl.SelectedItem = searchLogTab; } public void Delete() @@ -705,9 +1005,15 @@ public void Copy() } } - public void CopySubtree() + public void CopySubtree(TreeView tree = null) { - if (treeView.SelectedItem is BaseNode treeNode) + tree = tree ?? ActiveTreeView; + if (tree == null) + { + return; + } + + if (tree.SelectedItem is BaseNode treeNode) { var text = Microsoft.Build.Logging.StructuredLogger.StringWriter.GetString(treeNode); CopyToClipboard(text); @@ -723,9 +1029,9 @@ public void SortChildren() } } - private void CopyAll() + private void CopyAll(TreeView tree = null) { - var tree = ActiveTreeView; + tree = tree ?? ActiveTreeView; if (tree == null) { return; @@ -735,7 +1041,34 @@ private void CopyAll() foreach (var item in tree.Items.OfType()) { var text = Microsoft.Build.Logging.StructuredLogger.StringWriter.GetString(item); - sb.AppendLine(text); + sb.Append(text); + if (!text.Contains("\n")) + { + sb.AppendLine(); + } + } + + CopyToClipboard(sb.ToString()); + } + + private void CopyPaths(TreeView tree = null) + { + tree = tree ?? ActiveTreeView; + if (tree == null) + { + return; + } + + var sb = new StringBuilder(); + foreach (var item in tree.Items.OfType()) + { + item.VisitAllChildren(s => + { + if (s is SourceFile file && !string.IsNullOrEmpty(file.SourceFilePath)) + { + sb.AppendLine(file.SourceFilePath); + } + }); } CopyToClipboard(sb.ToString()); @@ -813,6 +1146,12 @@ private bool CanView(BaseNode node) || (node is TextNode tn && tn.IsTextShortened); } + private bool HasFullText(BaseNode node) + { + return (node is NameValueNode nvn && nvn.IsValueShortened) + || (node is TextNode tn && tn.IsTextShortened); + } + private bool Invoke(BaseNode treeNode) { if (treeNode == null) @@ -845,7 +1184,11 @@ private bool Invoke(BaseNode treeNode) case Target target: return DisplayTarget(target.SourceFilePath, target.Name); case Task task: - return DisplayTask(task.SourceFilePath, task.Parent, task.Name); + return DisplayTask(task); + case AddItem addItem: + return DisplayAddRemoveItem(addItem.Parent, addItem.LineNumber ?? 0); + case RemoveItem removeItem: + return DisplayAddRemoveItem(removeItem.Parent, removeItem.LineNumber ?? 0); case IHasSourceFile hasSourceFile when hasSourceFile.SourceFilePath != null: int line = 0; var hasLine = hasSourceFile as IHasLineNumber; @@ -888,25 +1231,51 @@ public bool DisplayFile(string sourceFilePath, int lineNumber = 0, int column = return false; } - Action preprocess = preprocessedFileManager.GetPreprocessAction(sourceFilePath, PreprocessedFileManager.GetEvaluationKey(evaluation)); - documentWell.DisplaySource(sourceFilePath, text.Text, lineNumber, column, preprocess, navigationHelper); + string preprocessableFilePath = Utilities.InsertMissingDriveSeparator(sourceFilePath); + + Action preprocess = null; + if (evaluation != null) + { + preprocess = preprocessedFileManager.GetPreprocessAction(preprocessableFilePath, PreprocessedFileManager.GetEvaluationKey(evaluation)); + } + + documentWell.DisplaySource(preprocessableFilePath, text.Text, lineNumber, column, preprocess, navigationHelper); return true; } public bool DisplayText(string text, string caption = null) { + caption = TextUtilities.SanitizeFileName(caption); documentWell.DisplaySource(caption ?? "Text", text, displayPath: false); return true; } - private bool DisplayTask(string sourceFilePath, TreeNode parent, string name) + private bool DisplayAddRemoveItem(TreeNode parent, int line) + { + if (parent is not Target target) + { + return false; + } + + string sourceFilePath = target.SourceFilePath; + return DisplayFile(sourceFilePath, line); + } + + private bool DisplayTask(Task task) { - Target target = parent as Target; - if (target == null) + var sourceFilePath = task.SourceFilePath; + var parent = task.Parent; + var name = task.Name; + if (parent is not Target target) { return DisplayFile(sourceFilePath); } + if (task.LineNumber.HasValue && task.LineNumber.Value > 0) + { + return DisplayFile(sourceFilePath, task.LineNumber.Value); + } + return DisplayTarget(sourceFilePath, target.Name, name); } @@ -918,8 +1287,8 @@ public bool DisplayTarget(string sourceFilePath, string targetName, string taskN return false; } - var xml = text.XmlRoot; - IXmlElement root = xml.Root; + var xml = text.XmlRoot.Root; + IXmlElement root = xml; int startPosition = 0; int line = 0; @@ -1029,5 +1398,10 @@ private void TreeViewItem_Selected(object sender, RoutedEventArgs e) { SelectedTreeViewItem = e.Source as TreeViewItem; } + + public override string ToString() + { + return Build?.ToString(); + } } } diff --git a/src/StructuredLogViewer.Avalonia/Controls/DocumentWell.xaml b/src/StructuredLogViewer.Avalonia/Controls/DocumentWell.xaml index c99dc8ec..65a94e55 100644 --- a/src/StructuredLogViewer.Avalonia/Controls/DocumentWell.xaml +++ b/src/StructuredLogViewer.Avalonia/Controls/DocumentWell.xaml @@ -6,40 +6,40 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + -