Files
godot/modules/mono/editor/GodotTools/GodotTools/Build/BuildProblemsView.cs
Nông Văn Tình ae6079dbdf Fix incorrect condition for error filtering
Fixes: #87643

The original condition stopped immediately after checking for 'searchText' in the 'Message' field, resulting in premature termination of subsequent checks. This fix ensures that all relevant conditions are appropriately evaluated before determining the filtering outcome.

Additionally, accompanying changes include improved code readability for better comprehension. This adjustment enhances the maintainability of the error filtering mechanism, contributing to a more robust codebase overall.

(cherry picked from commit d81c9c32c5)
2024-03-11 16:39:27 +01:00

695 lines
25 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Godot;
using GodotTools.Internals;
using static GodotTools.Internals.Globals;
using FileAccess = Godot.FileAccess;
#nullable enable
namespace GodotTools.Build
{
public partial class BuildProblemsView : HBoxContainer
{
#nullable disable
private Button _clearButton;
private Button _copyButton;
private Button _toggleLayoutButton;
private Button _showSearchButton;
private LineEdit _searchBox;
#nullable enable
private readonly Dictionary<BuildDiagnostic.DiagnosticType, BuildProblemsFilter> _filtersByType = new();
#nullable disable
private Tree _problemsTree;
private PopupMenu _problemsContextMenu;
#nullable enable
public enum ProblemsLayout { List, Tree }
private ProblemsLayout _layout = ProblemsLayout.Tree;
private readonly List<BuildDiagnostic> _diagnostics = new();
public int TotalDiagnosticCount => _diagnostics.Count;
private readonly Dictionary<BuildDiagnostic.DiagnosticType, int> _problemCountByType = new();
public int WarningCount =>
GetProblemCountForType(BuildDiagnostic.DiagnosticType.Warning);
public int ErrorCount =>
GetProblemCountForType(BuildDiagnostic.DiagnosticType.Error);
private int GetProblemCountForType(BuildDiagnostic.DiagnosticType type)
{
if (!_problemCountByType.TryGetValue(type, out int count))
{
count = _diagnostics.Count(d => d.Type == type);
_problemCountByType[type] = count;
}
return count;
}
private static IEnumerable<BuildDiagnostic> ReadDiagnosticsFromFile(string csvFile)
{
using var file = FileAccess.Open(csvFile, FileAccess.ModeFlags.Read);
if (file == null)
yield break;
while (!file.EofReached())
{
string[] csvColumns = file.GetCsvLine();
if (csvColumns.Length == 1 && string.IsNullOrEmpty(csvColumns[0]))
yield break;
if (csvColumns.Length != 7)
{
GD.PushError($"Expected 7 columns, got {csvColumns.Length}");
continue;
}
var diagnostic = new BuildDiagnostic
{
Type = csvColumns[0] switch
{
"warning" => BuildDiagnostic.DiagnosticType.Warning,
"error" or _ => BuildDiagnostic.DiagnosticType.Error,
},
File = csvColumns[1],
Line = int.Parse(csvColumns[2]),
Column = int.Parse(csvColumns[3]),
Code = csvColumns[4],
Message = csvColumns[5],
ProjectFile = csvColumns[6],
};
// If there's no ProjectFile but the File is a csproj, then use that.
if (string.IsNullOrEmpty(diagnostic.ProjectFile) &&
!string.IsNullOrEmpty(diagnostic.File) &&
diagnostic.File.EndsWith(".csproj"))
{
diagnostic.ProjectFile = diagnostic.File;
}
yield return diagnostic;
}
}
public void SetDiagnosticsFromFile(string csvFile)
{
var diagnostics = ReadDiagnosticsFromFile(csvFile);
SetDiagnostics(diagnostics);
}
public void SetDiagnostics(IEnumerable<BuildDiagnostic> diagnostics)
{
_diagnostics.Clear();
_problemCountByType.Clear();
_diagnostics.AddRange(diagnostics);
UpdateProblemsView();
}
public void Clear()
{
_problemsTree.Clear();
_diagnostics.Clear();
_problemCountByType.Clear();
UpdateProblemsView();
}
private void CopySelectedProblems()
{
var selectedItem = _problemsTree.GetNextSelected(null);
if (selectedItem == null)
return;
var selectedIdxs = new List<int>();
while (selectedItem != null)
{
int selectedIdx = (int)selectedItem.GetMetadata(0);
selectedIdxs.Add(selectedIdx);
selectedItem = _problemsTree.GetNextSelected(selectedItem);
}
if (selectedIdxs.Count == 0)
return;
var selectedDiagnostics = selectedIdxs.Select(i => _diagnostics[i]);
var sb = new StringBuilder();
foreach (var diagnostic in selectedDiagnostics)
{
if (!string.IsNullOrEmpty(diagnostic.Code))
sb.Append($"{diagnostic.Code}: ");
sb.AppendLine($"{diagnostic.Message} {diagnostic.File}({diagnostic.Line},{diagnostic.Column})");
}
string text = sb.ToString();
if (!string.IsNullOrEmpty(text))
DisplayServer.ClipboardSet(text);
}
private void ToggleLayout(bool pressed)
{
_layout = pressed ? ProblemsLayout.List : ProblemsLayout.Tree;
var editorSettings = EditorInterface.Singleton.GetEditorSettings();
editorSettings.SetSetting(GodotSharpEditor.Settings.ProblemsLayout, Variant.From(_layout));
_toggleLayoutButton.Icon = GetToggleLayoutIcon();
_toggleLayoutButton.TooltipText = GetToggleLayoutTooltipText();
UpdateProblemsView();
}
private bool GetToggleLayoutPressedState()
{
// If pressed: List layout.
// If not pressed: Tree layout.
return _layout == ProblemsLayout.List;
}
private Texture2D? GetToggleLayoutIcon()
{
return _layout switch
{
ProblemsLayout.List => GetThemeIcon("FileList", "EditorIcons"),
ProblemsLayout.Tree or _ => GetThemeIcon("FileTree", "EditorIcons"),
};
}
private string GetToggleLayoutTooltipText()
{
return _layout switch
{
ProblemsLayout.List => "View as a Tree".TTR(),
ProblemsLayout.Tree or _ => "View as a List".TTR(),
};
}
private void ToggleSearchBoxVisibility(bool pressed)
{
_searchBox.Visible = pressed;
if (pressed)
{
_searchBox.GrabFocus();
}
}
private void SearchTextChanged(string text)
{
UpdateProblemsView();
}
private void ToggleFilter(bool pressed)
{
UpdateProblemsView();
}
private void GoToSelectedProblem()
{
var selectedItem = _problemsTree.GetSelected();
if (selectedItem == null)
throw new InvalidOperationException("Item tree has no selected items.");
// Get correct diagnostic index from problems tree.
int diagnosticIndex = (int)selectedItem.GetMetadata(0);
if (diagnosticIndex < 0 || diagnosticIndex >= _diagnostics.Count)
throw new InvalidOperationException("Diagnostic index out of range.");
var diagnostic = _diagnostics[diagnosticIndex];
if (string.IsNullOrEmpty(diagnostic.ProjectFile) && string.IsNullOrEmpty(diagnostic.File))
return;
string? projectDir = !string.IsNullOrEmpty(diagnostic.ProjectFile) ?
diagnostic.ProjectFile.GetBaseDir() :
GodotSharpEditor.Instance.MSBuildPanel.LastBuildInfo?.Solution.GetBaseDir();
if (string.IsNullOrEmpty(projectDir))
return;
string file = Path.Combine(projectDir.SimplifyGodotPath(), diagnostic.File.SimplifyGodotPath());
if (!File.Exists(file))
return;
file = ProjectSettings.LocalizePath(file);
if (file.StartsWith("res://"))
{
var script = (Script)ResourceLoader.Load(file, typeHint: Internal.CSharpLanguageType);
// Godot's ScriptEditor.Edit is 0-based but the diagnostic lines are 1-based.
if (script != null && Internal.ScriptEditorEdit(script, diagnostic.Line - 1, diagnostic.Column - 1))
Internal.EditorNodeShowScriptScreen();
}
}
private void ShowProblemContextMenu(Vector2 position, long mouseButtonIndex)
{
if (mouseButtonIndex != (long)MouseButton.Right)
return;
_problemsContextMenu.Clear();
_problemsContextMenu.Size = new Vector2I(1, 1);
var selectedItem = _problemsTree.GetSelected();
if (selectedItem != null)
{
// Add menu entries for the selected item.
_problemsContextMenu.AddIconItem(GetThemeIcon("ActionCopy", "EditorIcons"),
label: "Copy Error".TTR(), (int)ProblemContextMenuOption.Copy);
}
if (_problemsContextMenu.ItemCount > 0)
{
_problemsContextMenu.Position = (Vector2I)(_problemsTree.GlobalPosition + position);
_problemsContextMenu.Popup();
}
}
private enum ProblemContextMenuOption
{
Copy,
}
private void ProblemContextOptionPressed(long id)
{
switch ((ProblemContextMenuOption)id)
{
case ProblemContextMenuOption.Copy:
CopySelectedProblems();
break;
default:
throw new ArgumentOutOfRangeException(nameof(id), id, "Invalid problem context menu option.");
}
}
private bool ShouldDisplayDiagnostic(BuildDiagnostic diagnostic)
{
if (!_filtersByType[diagnostic.Type].IsActive)
return false;
string searchText = _searchBox.Text;
if (string.IsNullOrEmpty(searchText))
return true;
if (diagnostic.Message.Contains(searchText, StringComparison.OrdinalIgnoreCase))
return true;
if (diagnostic.File?.Contains(searchText, StringComparison.OrdinalIgnoreCase) ?? false)
return true;
return false;
}
private Color? GetProblemItemColor(BuildDiagnostic diagnostic)
{
return diagnostic.Type switch
{
BuildDiagnostic.DiagnosticType.Warning => GetThemeColor("warning_color", "Editor"),
BuildDiagnostic.DiagnosticType.Error => GetThemeColor("error_color", "Editor"),
_ => null,
};
}
public void UpdateProblemsView()
{
switch (_layout)
{
case ProblemsLayout.List:
UpdateProblemsList();
break;
case ProblemsLayout.Tree:
default:
UpdateProblemsTree();
break;
}
foreach (var (type, filter) in _filtersByType)
{
int count = _diagnostics.Count(d => d.Type == type);
filter.ProblemsCount = count;
}
if (_diagnostics.Count == 0)
Name = "Problems".TTR();
else
Name = $"{"Problems".TTR()} ({_diagnostics.Count})";
}
private void UpdateProblemsList()
{
_problemsTree.Clear();
var root = _problemsTree.CreateItem();
for (int i = 0; i < _diagnostics.Count; i++)
{
var diagnostic = _diagnostics[i];
if (!ShouldDisplayDiagnostic(diagnostic))
continue;
var item = CreateProblemItem(diagnostic, includeFileInText: true);
var problemItem = _problemsTree.CreateItem(root);
problemItem.SetIcon(0, item.Icon);
problemItem.SetText(0, item.Text);
problemItem.SetTooltipText(0, item.TooltipText);
problemItem.SetMetadata(0, i);
var color = GetProblemItemColor(diagnostic);
if (color.HasValue)
problemItem.SetCustomColor(0, color.Value);
}
}
private void UpdateProblemsTree()
{
_problemsTree.Clear();
var root = _problemsTree.CreateItem();
var groupedDiagnostics = _diagnostics.Select((d, i) => (Diagnostic: d, Index: i))
.Where(x => ShouldDisplayDiagnostic(x.Diagnostic))
.GroupBy(x => x.Diagnostic.ProjectFile)
.Select(g => (ProjectFile: g.Key, Diagnostics: g.GroupBy(x => x.Diagnostic.File)
.Select(x => (File: x.Key, Diagnostics: x.ToArray()))))
.ToArray();
if (groupedDiagnostics.Length == 0)
return;
foreach (var (projectFile, projectDiagnostics) in groupedDiagnostics)
{
TreeItem projectItem;
if (groupedDiagnostics.Length == 1)
{
// Don't create a project item if there's only one project.
projectItem = root;
}
else
{
string projectFilePath = !string.IsNullOrEmpty(projectFile)
? projectFile
: "Unknown project".TTR();
projectItem = _problemsTree.CreateItem(root);
projectItem.SetText(0, projectFilePath);
projectItem.SetSelectable(0, false);
}
foreach (var (file, fileDiagnostics) in projectDiagnostics)
{
if (fileDiagnostics.Length == 0)
continue;
string? projectDir = Path.GetDirectoryName(projectFile);
string relativeFilePath = !string.IsNullOrEmpty(file) && !string.IsNullOrEmpty(projectDir)
? Path.GetRelativePath(projectDir, file)
: "Unknown file".TTR();
string fileItemText = string.Format("{0} ({1} issues)".TTR(), relativeFilePath, fileDiagnostics.Length);
var fileItem = _problemsTree.CreateItem(projectItem);
fileItem.SetText(0, fileItemText);
fileItem.SetSelectable(0, false);
foreach (var (diagnostic, index) in fileDiagnostics)
{
var item = CreateProblemItem(diagnostic);
var problemItem = _problemsTree.CreateItem(fileItem);
problemItem.SetIcon(0, item.Icon);
problemItem.SetText(0, item.Text);
problemItem.SetTooltipText(0, item.TooltipText);
problemItem.SetMetadata(0, index);
var color = GetProblemItemColor(diagnostic);
if (color.HasValue)
problemItem.SetCustomColor(0, color.Value);
}
}
}
}
private class ProblemItem
{
public string? Text { get; set; }
public string? TooltipText { get; set; }
public Texture2D? Icon { get; set; }
}
private ProblemItem CreateProblemItem(BuildDiagnostic diagnostic, bool includeFileInText = false)
{
var text = new StringBuilder();
var tooltip = new StringBuilder();
ReadOnlySpan<char> shortMessage = diagnostic.Message.AsSpan();
int lineBreakIdx = shortMessage.IndexOf('\n');
if (lineBreakIdx != -1)
shortMessage = shortMessage[..lineBreakIdx];
text.Append(shortMessage);
tooltip.Append($"Message: {diagnostic.Message}");
if (!string.IsNullOrEmpty(diagnostic.Code))
tooltip.Append($"\nCode: {diagnostic.Code}");
string type = diagnostic.Type switch
{
BuildDiagnostic.DiagnosticType.Hidden => "hidden",
BuildDiagnostic.DiagnosticType.Info => "info",
BuildDiagnostic.DiagnosticType.Warning => "warning",
BuildDiagnostic.DiagnosticType.Error => "error",
_ => "unknown",
};
tooltip.Append($"\nType: {type}");
if (!string.IsNullOrEmpty(diagnostic.File))
{
text.Append(' ');
if (includeFileInText)
{
text.Append(diagnostic.File);
}
text.Append($"({diagnostic.Line},{diagnostic.Column})");
tooltip.Append($"\nFile: {diagnostic.File}");
tooltip.Append($"\nLine: {diagnostic.Line}");
tooltip.Append($"\nColumn: {diagnostic.Column}");
}
if (!string.IsNullOrEmpty(diagnostic.ProjectFile))
tooltip.Append($"\nProject: {diagnostic.ProjectFile}");
return new ProblemItem()
{
Text = text.ToString(),
TooltipText = tooltip.ToString(),
Icon = diagnostic.Type switch
{
BuildDiagnostic.DiagnosticType.Warning => GetThemeIcon("Warning", "EditorIcons"),
BuildDiagnostic.DiagnosticType.Error => GetThemeIcon("Error", "EditorIcons"),
_ => null,
},
};
}
public override void _Ready()
{
var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
_layout = editorSettings.GetSetting(GodotSharpEditor.Settings.ProblemsLayout).As<ProblemsLayout>();
Name = "Problems".TTR();
var vbLeft = new VBoxContainer
{
CustomMinimumSize = new Vector2(0, 180 * EditorScale),
SizeFlagsVertical = SizeFlags.ExpandFill,
SizeFlagsHorizontal = SizeFlags.ExpandFill,
};
AddChild(vbLeft);
// Problem Tree.
_problemsTree = new Tree
{
SizeFlagsVertical = SizeFlags.ExpandFill,
SizeFlagsHorizontal = SizeFlags.ExpandFill,
AllowRmbSelect = true,
HideRoot = true,
};
_problemsTree.ItemActivated += GoToSelectedProblem;
_problemsTree.ItemMouseSelected += ShowProblemContextMenu;
vbLeft.AddChild(_problemsTree);
// Problem context menu.
_problemsContextMenu = new PopupMenu();
_problemsContextMenu.IdPressed += ProblemContextOptionPressed;
_problemsTree.AddChild(_problemsContextMenu);
// Search box.
_searchBox = new LineEdit
{
SizeFlagsHorizontal = SizeFlags.ExpandFill,
PlaceholderText = "Filter Problems".TTR(),
ClearButtonEnabled = true,
};
_searchBox.TextChanged += SearchTextChanged;
vbLeft.AddChild(_searchBox);
var vbRight = new VBoxContainer();
AddChild(vbRight);
// Tools grid.
var hbTools = new HBoxContainer
{
SizeFlagsHorizontal = SizeFlags.ExpandFill,
};
vbRight.AddChild(hbTools);
// Clear.
_clearButton = new Button
{
ThemeTypeVariation = "FlatButton",
FocusMode = FocusModeEnum.None,
Shortcut = EditorDefShortcut("editor/clear_output", "Clear Output".TTR(), (Key)KeyModifierMask.MaskCmdOrCtrl | (Key)KeyModifierMask.MaskShift | Key.K),
ShortcutContext = this,
};
_clearButton.Pressed += Clear;
hbTools.AddChild(_clearButton);
// Copy.
_copyButton = new Button
{
ThemeTypeVariation = "FlatButton",
FocusMode = FocusModeEnum.None,
Shortcut = EditorDefShortcut("editor/copy_output", "Copy Selection".TTR(), (Key)KeyModifierMask.MaskCmdOrCtrl | Key.C),
ShortcutContext = this,
};
_copyButton.Pressed += CopySelectedProblems;
hbTools.AddChild(_copyButton);
// A second hbox to make a 2x2 grid of buttons.
var hbTools2 = new HBoxContainer
{
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
};
vbRight.AddChild(hbTools2);
// Toggle List/Tree.
_toggleLayoutButton = new Button
{
Flat = true,
FocusMode = FocusModeEnum.None,
TooltipText = GetToggleLayoutTooltipText(),
ToggleMode = true,
ButtonPressed = GetToggleLayoutPressedState(),
};
// Don't tint the icon even when in "pressed" state.
_toggleLayoutButton.AddThemeColorOverride("icon_pressed_color", Colors.White);
_toggleLayoutButton.Toggled += ToggleLayout;
hbTools2.AddChild(_toggleLayoutButton);
// Show Search.
_showSearchButton = new Button
{
ThemeTypeVariation = "FlatButton",
FocusMode = FocusModeEnum.None,
ToggleMode = true,
ButtonPressed = true,
Shortcut = EditorDefShortcut("editor/open_search", "Focus Search/Filter Bar".TTR(), (Key)KeyModifierMask.MaskCmdOrCtrl | Key.F),
ShortcutContext = this,
};
_showSearchButton.Toggled += ToggleSearchBoxVisibility;
hbTools2.AddChild(_showSearchButton);
// Diagnostic Type Filters.
vbRight.AddChild(new HSeparator());
var infoFilter = new BuildProblemsFilter(BuildDiagnostic.DiagnosticType.Info);
infoFilter.ToggleButton.TooltipText = "Toggle visibility of info diagnostics.".TTR();
infoFilter.ToggleButton.Toggled += ToggleFilter;
vbRight.AddChild(infoFilter.ToggleButton);
_filtersByType[BuildDiagnostic.DiagnosticType.Info] = infoFilter;
var errorFilter = new BuildProblemsFilter(BuildDiagnostic.DiagnosticType.Error);
errorFilter.ToggleButton.TooltipText = "Toggle visibility of errors.".TTR();
errorFilter.ToggleButton.Toggled += ToggleFilter;
vbRight.AddChild(errorFilter.ToggleButton);
_filtersByType[BuildDiagnostic.DiagnosticType.Error] = errorFilter;
var warningFilter = new BuildProblemsFilter(BuildDiagnostic.DiagnosticType.Warning);
warningFilter.ToggleButton.TooltipText = "Toggle visibility of warnings.".TTR();
warningFilter.ToggleButton.Toggled += ToggleFilter;
vbRight.AddChild(warningFilter.ToggleButton);
_filtersByType[BuildDiagnostic.DiagnosticType.Warning] = warningFilter;
UpdateTheme();
UpdateProblemsView();
}
public override void _Notification(int what)
{
base._Notification(what);
switch ((long)what)
{
case EditorSettings.NotificationEditorSettingsChanged:
var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
_layout = editorSettings.GetSetting(GodotSharpEditor.Settings.ProblemsLayout).As<ProblemsLayout>();
_toggleLayoutButton.ButtonPressed = GetToggleLayoutPressedState();
UpdateProblemsView();
break;
case NotificationThemeChanged:
UpdateTheme();
break;
}
}
private void UpdateTheme()
{
// Nodes will be null until _Ready is called.
if (_clearButton == null)
return;
foreach (var (type, filter) in _filtersByType)
{
filter.ToggleButton.Icon = type switch
{
BuildDiagnostic.DiagnosticType.Info => GetThemeIcon("Popup", "EditorIcons"),
BuildDiagnostic.DiagnosticType.Warning => GetThemeIcon("StatusWarning", "EditorIcons"),
BuildDiagnostic.DiagnosticType.Error => GetThemeIcon("StatusError", "EditorIcons"),
_ => null,
};
}
_clearButton.Icon = GetThemeIcon("Clear", "EditorIcons");
_copyButton.Icon = GetThemeIcon("ActionCopy", "EditorIcons");
_toggleLayoutButton.Icon = GetToggleLayoutIcon();
_showSearchButton.Icon = GetThemeIcon("Search", "EditorIcons");
_searchBox.RightIcon = GetThemeIcon("Search", "EditorIcons");
}
}
}