From 250665d68b3ed6e20fc4d4ab0fa81beb951f6873 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Sat, 29 Jun 2024 22:24:24 +0200 Subject: [PATCH 01/23] LSP: Implement semantic token support for CodeWindows #686 This also heavily refactors our existing scanner infrastructure to accomodate both Antlr based lexers and LSP-based semantic token providers. As the latter also support supplying token modifiers, support for this has been added as well (e.g., rendering static things in italics). --- Assets/SEE/Controls/Actions/ShowCodeAction.cs | 60 +- Assets/SEE/DataModel/DG/IO/LSPImporter.cs | 2 +- ...vider.cs.meta => GXLGraphProvider.cs.meta} | 0 Assets/SEE/GraphProviders/LSPGraphProvider.cs | 10 +- Assets/SEE/GraphProviders/VCSGraphProvider.cs | 23 +- .../SEE/Net/Dashboard/Model/Issues/Issue.cs | 2 +- Assets/SEE/Scanner/Antlr.meta | 3 + Assets/SEE/Scanner/Antlr/AntlrLanguage.cs | 582 +++++++++++++++++ .../SEE/Scanner/Antlr/AntlrLanguage.cs.meta | 11 + Assets/SEE/Scanner/Antlr/AntlrToken.cs | 71 +++ Assets/SEE/Scanner/Antlr/AntlrToken.cs.meta | 11 + Assets/SEE/Scanner/Antlr/AntlrTokenType.cs | 84 +++ .../SEE/Scanner/Antlr/AntlrTokenType.cs.meta | 3 + Assets/SEE/Scanner/LSP.meta | 3 + Assets/SEE/Scanner/LSP/LSPToken.cs | 174 ++++++ Assets/SEE/Scanner/LSP/LSPToken.cs.meta | 3 + Assets/SEE/Scanner/LSP/LSPTokenType.cs | 170 +++++ Assets/SEE/Scanner/LSP/LSPTokenType.cs.meta | 3 + Assets/SEE/Scanner/SEEToken.cs | 261 +------- Assets/SEE/Scanner/TokenLanguage.cs | 584 +----------------- Assets/SEE/Scanner/TokenLanguage.cs.meta | 6 +- Assets/SEE/Scanner/TokenMetrics.cs | 39 +- Assets/SEE/Scanner/TokenModifiers.cs | 132 ++++ Assets/SEE/Scanner/TokenModifiers.cs.meta | 3 + Assets/SEE/Scanner/TokenType.cs | 51 ++ Assets/SEE/Scanner/TokenType.cs.meta | 3 + Assets/SEE/Tools/LSP/LSPHandler.cs | 87 ++- Assets/SEE/Tools/LSP/LSPLanguage.cs | 29 +- Assets/SEE/Tools/LSP/LSPServer.cs | 49 +- ...DebugAdapterProtocolSessionCodePosition.cs | 3 +- Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs | 13 +- .../CodeWindow/{Input => }/CodeWindowInput.cs | 123 ++-- .../Window/CodeWindow/CodeWindowInput.cs.meta | 11 + .../UI/Window/CodeWindow/DesktopCodeWindow.cs | 5 +- Assets/SEE/UI/Window/CodeWindow/Input.meta | 3 - .../CodeWindow/Input/CodeWindowInput.cs.meta | 3 - Assets/SEE/Utils/AsyncUtils.cs | 2 +- Assets/SEETests/TestTokenMetrics.cs | 11 +- 38 files changed, 1639 insertions(+), 994 deletions(-) rename Assets/SEE/GraphProviders/{GxlGraphProvider.cs.meta => GXLGraphProvider.cs.meta} (100%) create mode 100644 Assets/SEE/Scanner/Antlr.meta create mode 100644 Assets/SEE/Scanner/Antlr/AntlrLanguage.cs create mode 100644 Assets/SEE/Scanner/Antlr/AntlrLanguage.cs.meta create mode 100644 Assets/SEE/Scanner/Antlr/AntlrToken.cs create mode 100644 Assets/SEE/Scanner/Antlr/AntlrToken.cs.meta create mode 100644 Assets/SEE/Scanner/Antlr/AntlrTokenType.cs create mode 100644 Assets/SEE/Scanner/Antlr/AntlrTokenType.cs.meta create mode 100644 Assets/SEE/Scanner/LSP.meta create mode 100644 Assets/SEE/Scanner/LSP/LSPToken.cs create mode 100644 Assets/SEE/Scanner/LSP/LSPToken.cs.meta create mode 100644 Assets/SEE/Scanner/LSP/LSPTokenType.cs create mode 100644 Assets/SEE/Scanner/LSP/LSPTokenType.cs.meta create mode 100644 Assets/SEE/Scanner/TokenModifiers.cs create mode 100644 Assets/SEE/Scanner/TokenModifiers.cs.meta create mode 100644 Assets/SEE/Scanner/TokenType.cs create mode 100644 Assets/SEE/Scanner/TokenType.cs.meta rename Assets/SEE/UI/Window/CodeWindow/{Input => }/CodeWindowInput.cs (82%) create mode 100644 Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs.meta delete mode 100644 Assets/SEE/UI/Window/CodeWindow/Input.meta delete mode 100644 Assets/SEE/UI/Window/CodeWindow/Input/CodeWindowInput.cs.meta diff --git a/Assets/SEE/Controls/Actions/ShowCodeAction.cs b/Assets/SEE/Controls/Actions/ShowCodeAction.cs index e181ef46d0..3382aad76b 100644 --- a/Assets/SEE/Controls/Actions/ShowCodeAction.cs +++ b/Assets/SEE/Controls/Actions/ShowCodeAction.cs @@ -9,6 +9,7 @@ using UnityEngine; using SEE.DataModel.DG; using System; +using Cysharp.Threading.Tasks; using SEE.UI.Window; using SEE.Utils.History; using SEE.Game.City; @@ -254,36 +255,38 @@ public static CodeWindow ShowVCSDiff(GraphElementRef graphElementRef, DiffCity c public static CodeWindow ShowCode(GraphElementRef graphElementRef) { GraphElement graphElement = graphElementRef.Elem; - CodeWindow codeWindow; - if (graphElement.TryGetCommitID(out string commitID)) + CodeWindow codeWindow = GetOrCreateCodeWindow(graphElementRef, graphElement.Filename); + EnterWindowContent().Forget(); // This can happen in the background. + return codeWindow; + + async UniTaskVoid EnterWindowContent() { - codeWindow = GetOrCreateCodeWindow(graphElementRef, graphElement.Filename); - if (!graphElement.TryGetRepositoryPath(out string repositoryPath)) + // We have to differentiate between a file-based and a VCS-based code city. + if (graphElement.TryGetCommitID(out string commitID)) { - string message = $"Selected {GetName(graphElement)} has no repository path."; - ShowNotification.Error("No repository path", message, log: false); - throw new InvalidOperationException(message); + if (!graphElement.TryGetRepositoryPath(out string repositoryPath)) + { + string message = $"Selected {GetName(graphElement)} has no repository path."; + ShowNotification.Error("No repository path", message, log: false); + throw new InvalidOperationException(message); + } + IVersionControl vcs = VersionControlFactory.GetVersionControl(VCSKind.Git, repositoryPath); + string[] fileContent = vcs.Show(graphElement.ID, commitID). + Split("\\n", StringSplitOptions.RemoveEmptyEntries); + codeWindow.EnterFromText(fileContent); + } + else + { + await codeWindow.EnterFromFileAsync(GetPath(graphElement).absolutePlatformPath); } - IVersionControl vcs = VersionControlFactory.GetVersionControl(VCSKind.Git, repositoryPath); - string[] fileContent = vcs.Show(graphElement.ID, commitID). - Split("\\n", StringSplitOptions.RemoveEmptyEntries); - codeWindow.EnterFromText(fileContent); - } - else - { - (string filename, string absolutePlatformPath) = GetPath(graphElement); - codeWindow = GetOrCreateCodeWindow(graphElementRef, filename); - // File name of source code file to read from it - codeWindow.EnterFromFile(absolutePlatformPath); - } - // Pass line number to automatically scroll to it, if it exists - if (graphElement.SourceLine is { } line) - { - codeWindow.ScrolledVisibleLine = line; + // Pass line number to automatically scroll to it, if it exists + if (graphElement.SourceLine is { } line) + { + codeWindow.ScrolledVisibleLine = line; + } } - return codeWindow; } public override bool Update() @@ -299,6 +302,13 @@ public override bool Update() return false; } + ShowCodeWindow(); + } + + return false; + + void ShowCodeWindow() + { // Edges of type Clone will be handled differently. For these, we will be // showing a unified diff. CodeWindow codeWindow = graphElementRef is EdgeRef { Value: { Type: "Clone" } } edgeRef @@ -313,8 +323,6 @@ public override bool Update() manager.ActiveWindow = codeWindow; // TODO (#669): Set font size etc in settings (maybe, or maybe that's too much) } - - return false; } } } diff --git a/Assets/SEE/DataModel/DG/IO/LSPImporter.cs b/Assets/SEE/DataModel/DG/IO/LSPImporter.cs index 7a4b76f0cd..486c4d45c4 100644 --- a/Assets/SEE/DataModel/DG/IO/LSPImporter.cs +++ b/Assets/SEE/DataModel/DG/IO/LSPImporter.cs @@ -185,7 +185,7 @@ public async UniTask LoadAsync(Graph graph, Action changePercentage = nul CancellationToken token = default) { // Query all documents whose file extension is supported by the language server. - List relevantExtensions = Handler.Server.Languages.SelectMany(x => x.Extensions).ToList(); + List relevantExtensions = Handler.Server.Languages.SelectMany(x => x.FileExtensions).ToList(); List relevantDocuments = SourcePaths.SelectMany(RelevantDocumentsForPath) .Where(x => ExcludedPaths.All(y => !x.StartsWith(y))) .Distinct().ToList(); diff --git a/Assets/SEE/GraphProviders/GxlGraphProvider.cs.meta b/Assets/SEE/GraphProviders/GXLGraphProvider.cs.meta similarity index 100% rename from Assets/SEE/GraphProviders/GxlGraphProvider.cs.meta rename to Assets/SEE/GraphProviders/GXLGraphProvider.cs.meta diff --git a/Assets/SEE/GraphProviders/LSPGraphProvider.cs b/Assets/SEE/GraphProviders/LSPGraphProvider.cs index 926a3f2127..d07e86432f 100644 --- a/Assets/SEE/GraphProviders/LSPGraphProvider.cs +++ b/Assets/SEE/GraphProviders/LSPGraphProvider.cs @@ -105,6 +105,13 @@ public class LSPGraphProvider : SingleGraphProvider [EnumToggleButtons, FoldoutGroup("Import Settings")] public DiagnosticKind IncludedDiagnosticLevels = DiagnosticKind.All; + /// + /// If true, LSP functionality will be available in code windows. + /// + [Tooltip("If true, LSP functionality will be available in code windows."), RuntimeTab(GraphProviderFoldoutGroup)] + [LabelWidth(150)] + public bool UseInCodeWindows = true; + /// /// If true, the communication between the language server and SEE will be logged. /// @@ -139,7 +146,7 @@ public class LSPGraphProvider : SingleGraphProvider /// The available language servers as a dropdown list. private IEnumerable ServerDropdown() { - return LSPLanguage.All.Select(language => (language, LSPServer.All.Where(server => server.Languages.Contains(language)))) + return LSPLanguage.AllLspLanguages.Select(language => (language, LSPServer.All.Where(server => server.Languages.Contains(language)))) .SelectMany(pair => pair.Item2.Select(server => $"{pair.language}/{server}")) .OrderBy(server => server); } @@ -248,6 +255,7 @@ public override async UniTask ProvideAsync(Graph graph, AbstractSEECity c handler.Server = Server; handler.ProjectPath = ProjectPath.Path; handler.LogLSP = LogLSP; + handler.UseInCodeWindows = UseInCodeWindows; handler.TimeoutSpan = TimeSpan.FromSeconds(Timeout); await handler.InitializeAsync(executablePath: ServerPath ?? Server.ServerExecutable, token); if (token.IsCancellationRequested) diff --git a/Assets/SEE/GraphProviders/VCSGraphProvider.cs b/Assets/SEE/GraphProviders/VCSGraphProvider.cs index 7c75ec9459..931bce08be 100644 --- a/Assets/SEE/GraphProviders/VCSGraphProvider.cs +++ b/Assets/SEE/GraphProviders/VCSGraphProvider.cs @@ -15,6 +15,7 @@ using SEE.Scanner; using System.Threading; using Microsoft.Extensions.FileSystemGlobbing; +using SEE.Scanner.Antlr; namespace SEE.GraphProviders { @@ -264,7 +265,7 @@ public static Node BuildGraphFromPath(string path, Node parent, string parentPat } // Directory does not exist. - if (currentSegmentNode == null && pathSegments.Length > 1 && parent == null) + if (pathSegments.Length > 1 && parent == null) { rootNode.AddChild(NewNode(graph, pathSegments[0], directoryNodeType, pathSegments[0])); return BuildGraphFromPath(nodePath, graph.GetNode(pathSegments[0]), @@ -286,8 +287,7 @@ public static Node BuildGraphFromPath(string path, Node parent, string parentPat } // The node for the current pathSegment does not exist, and the node is a directory. - if (currentPathSegmentNode == null && - pathSegments.Length > 1) + if (pathSegments.Length > 1) { parent.AddChild(NewNode(graph, currentPathSegment, directoryNodeType, pathSegments[0])); return BuildGraphFromPath(nodePath, graph.GetNode(currentPathSegment), @@ -295,8 +295,7 @@ public static Node BuildGraphFromPath(string path, Node parent, string parentPat } // The node for the current pathSegment does not exist, and the node is a file. - if (currentPathSegmentNode == null && - pathSegments.Length == 1) + if (pathSegments.Length == 1) { Node addedFileNode = NewNode(graph, currentPathSegment, fileNodeType, pathSegments[0]); parent.AddChild(addedFileNode); @@ -342,21 +341,21 @@ private static List ListTree(LibGit2Sharp.Tree tree) /// The commitID where the files exist. /// The language the given text is written in. /// The token stream for the specified file and commit. - public static IEnumerable RetrieveTokens(string repositoryFilePath, Repository repository, - string commitID, TokenLanguage language) + public static ICollection RetrieveTokens(string repositoryFilePath, Repository repository, + string commitID, AntlrLanguage language) { Blob blob = repository.Lookup($"{commitID}:{repositoryFilePath}"); if (blob != null) { string fileContent = blob.GetContentText(); - return SEEToken.FromString(fileContent, language); + return AntlrToken.FromString(fileContent, language); } else { // Blob does not exist. Debug.LogWarning($"File {repositoryFilePath} does not exist.\n"); - return Enumerable.Empty(); + return new List(); } } @@ -375,10 +374,10 @@ private static void AddCodeMetrics(Graph graph, Repository repository, string co if (node.Type == fileNodeType) { string repositoryFilePath = node.ID; - TokenLanguage language = TokenLanguage.FromFileExtension(Path.GetExtension(repositoryFilePath).TrimStart('.')); - if (language != TokenLanguage.Plain) + AntlrLanguage language = AntlrLanguage.FromFileExtension(Path.GetExtension(repositoryFilePath).TrimStart('.')); + if (language != AntlrLanguage.Plain) { - IEnumerable tokens = RetrieveTokens(repositoryFilePath, repository, commitID, language); + ICollection tokens = RetrieveTokens(repositoryFilePath, repository, commitID, language); node.SetInt(Metrics.Prefix + "LOC", TokenMetrics.CalculateLinesOfCode(tokens)); node.SetInt(Metrics.Prefix + "McCabe_Complexity", TokenMetrics.CalculateMcCabeComplexity(tokens)); TokenMetrics.HalsteadMetrics halsteadMetrics = TokenMetrics.CalculateHalsteadMetrics(tokens); diff --git a/Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs b/Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs index a2df91d371..798f230554 100644 --- a/Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs +++ b/Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs @@ -39,7 +39,7 @@ public enum IssueState public IssueState State; /// - /// Whether or not the issue is suppressed or disabled via a control comment. + /// Whether the issue is suppressed or disabled via a control comment. /// /// /// This column is only available for projects where importing of suppressed issues is configured diff --git a/Assets/SEE/Scanner/Antlr.meta b/Assets/SEE/Scanner/Antlr.meta new file mode 100644 index 0000000000..9f35e31234 --- /dev/null +++ b/Assets/SEE/Scanner/Antlr.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 16346cc9b9ec430a9dfc96704b3868e2 +timeCreated: 1719432794 \ No newline at end of file diff --git a/Assets/SEE/Scanner/Antlr/AntlrLanguage.cs b/Assets/SEE/Scanner/Antlr/AntlrLanguage.cs new file mode 100644 index 0000000000..246bda12d8 --- /dev/null +++ b/Assets/SEE/Scanner/Antlr/AntlrLanguage.cs @@ -0,0 +1,582 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Antlr4.Runtime; + +namespace SEE.Scanner.Antlr +{ + /// + /// An Antlr-supported programming language a is in. + /// Symbolic names for the Antlr lexer are specified here. + /// + public class AntlrLanguage : TokenLanguage + { + /// + /// Name of the antlr lexer file the keywords were taken from. + /// + private string LexerFileName { get; } + + /// + /// Symbolic names for comments of a language, including block, line, and documentation comments. + /// + public ISet Comments { get; } + + /// + /// Symbolic names for keywords of a language. This also includes boolean literals and null literals. + /// + public ISet Keywords { get; } + + /// + /// Symbolic names for branch keywords of a language. + /// + public ISet BranchKeywords { get; } + + /// + /// Symbolic names for number literals of a language. This includes integer literals, floating point literals, etc. + /// + public ISet NumberLiterals { get; } + + /// + /// Symbolic names for string literals of a language. Also includes character literals. + /// + public ISet StringLiterals { get; } + + /// + /// Symbolic names for separators and operators of a language. + /// + public ISet Punctuation { get; } + + /// + /// Symbolic names for identifiers in a language. + /// + public ISet Identifiers { get; } + + /// + /// Symbolic names for whitespace in a language, excluding newlines. + /// + public ISet Whitespace { get; } + + /// + /// Symbolic names for newlines in a language. + /// + public ISet Newline { get; } + + #region Java Language + + /// + /// Name of the Java antlr grammar lexer. + /// + private const string javaFileName = "Java9Lexer.g4"; + + /// + /// Set of java file extensions. + /// + private static readonly HashSet javaExtensions = new() + { + "java" + }; + + /// + /// Set of antlr type names for Java keywords excluding . + /// + private static readonly HashSet javaKeywords = new() + { + "ABSTRACT", "ASSERT", "BOOLEAN", "BREAK", "BYTE", "CASE", "CATCH", "CHAR", "CLASS", "CONST", "CONTINUE", + "DEFAULT", "DO", "DOUBLE", "ELSE", "ENUM", "EXPORTS", "EXTENDS", "FINAL", "FINALLY", "FLOAT", + "GOTO", "IMPLEMENTS", "IMPORT", "INSTANCEOF", "INT", "INTERFACE", "LONG", "MODULE", "NATIVE", "NEW", + "OPEN", "OPERNS", "PACKAGE", "PRIVATE", "PROTECTED", "PROVIDES", "PUBLIC", "REQUIRES", "RETURN", "SHORT", + "STATIC", "STRICTFP", "SUPER", "SYNCHRONIZED", "THIS", "THROW", "THROWS", "TO", "TRANSIENT", + "TRANSITIVE", "USES", "VOID", "VOLATILE", "WITH", "UNDER_SCORE", + "BooleanLiteral", "NullLiteral" + }; + + /// + /// Set of antlr type names for Java branch keywords. + /// + private static readonly HashSet javaBranchKeywords = new() + { + "FOR", "IF", "SWITCH", "TRY", "WHILE" + }; + + /// + /// Set of antlr type names for Java integer and floating point literals. + /// + private static readonly HashSet javaNumbers = new() { "IntegerLiteral", "FloatingPointLiteral" }; + + /// Set of antlr type names for Java character and string literals. + private static readonly HashSet javaStrings = new() { "CharacterLiteral", "StringLiteral" }; + + /// Set of antlr type names for Java separators and operators. + private static readonly HashSet javaPunctuation = new() + { + "LPAREN", "RPAREN", "LBRACE", + "RBRACE", "LBRACK", "RBRACK", "SEMI", "COMMA", "DOT", "ELLIPSIS", "AT", "COLONCOLON", + "ASSIGN", "GT", "LT", "BANG", "TILDE", "QUESTION", "COLON", "ARROW", "EQUAL", "LE", "GE", "NOTEQUAL", "AND", + "OR", "INC", "DEC", "ADD", "SUB", "MUL", "DIV", "BITAND", "BITOR", "CARET", "MOD", + "ADD_ASSIGN", "SUB_ASSIGN", "MUL_ASSIGN", "DIV_ASSIGN", "AND_ASSIGN", "OR_ASSIGN", "XOR_ASSIGN", + "MOD_ASSIGN", "LSHIFT_ASSIGN", "RSHIFT_ASSIGN", "URSHIFT_ASSIGN" + }; + + /// Set of antlr type names for Java identifiers. + private static readonly HashSet javaIdentifiers = new() { "Identifier" }; + + /// + /// Set of antlr type names for Java whitespace. + /// + private static readonly HashSet javaWhitespace = new() { "WS" }; + + /// + /// Set of antlr type names for Java newlines. + /// + private static readonly HashSet javaNewlines = new() { "NEWLINE" }; + + /// + /// Set of antlr type names for Java comments. + /// + private static readonly HashSet javaComments = new() { "COMMENT", "LINE_COMMENT" }; + + #endregion + + #region C# Language + + /// + /// Name of the C# antlr grammar lexer. + /// + private const string cSharpFileName = "CSharpLexer.g4"; + + /// + /// Set of CSharp file extensions. + /// + private static readonly HashSet cSharpExtensions = new() + { + "cs" + }; + + /// + /// Set of antlr type names for CSharp keywords excluding . + /// + private static readonly HashSet cSharpKeywords = new() + { + // General keywords + "ABSTRACT", "ADD", "ALIAS", "ARGLIST", "AS", "ASCENDING", "ASYNC", "AWAIT", "BASE", "BOOL", "BREAK", "BY", + "BYTE", "CASE", "CATCH", "CHAR", "CHECKED", "CLASS", "CONST", "CONTINUE", "DECIMAL", "DEFAULT", "DELEGATE", + "DESCENDING", "DO", "DOUBLE", "DYNAMIC", "ELSE", "ENUM", "EQUALS", "EVENT", "EXPLICIT", "EXTERN", "FALSE", + "FINALLY", "FIXED", "FLOAT", "FROM", "GET", "GOTO", "GROUP", "IMPLICIT", "IN", "INT", + "INTERFACE", "INTERNAL", "INTO", "IS", "JOIN", "LET", "LOCK", "LONG", "NAMEOF", "NAMESPACE", "NEW", "NULL_", + "OBJECT", "ON", "OPERATOR", "ORDERBY", "OUT", "OVERRIDE", "PARAMS", "PARTIAL", "PRIVATE", "PROTECTED", + "PUBLIC", "READONLY", "REF", "REMOVE", "RETURN", "SBYTE", "SEALED", "SELECT", "SET", "SHORT", "SIZEOF", + "STACKALLOC", "STATIC", "STRING", "STRUCT", "THIS", "THROW", "TRUE", "TYPEOF", "UINT", + "ULONG", "UNCHECKED", "UNMANAGED", "UNSAFE", "USHORT", "USING", "VAR", "VIRTUAL", "VOID", "VOLATILE", "WHEN", + "WHERE", "YIELD", "SHARP", + // Directive keywords (anything within a directive is treated as a keyword, similar to IDEs + "DIRECTIVE_TRUE", "DIRECTIVE_FALSE", "DEFINE", "UNDEF", "DIRECTIVE_IF", + "ELIF", "DIRECTIVE_ELSE", "ENDIF", "LINE", "ERROR", "WARNING", "REGION", "ENDREGION", "PRAGMA", "NULLABLE", + "DIRECTIVE_DEFAULT", "DIRECTIVE_HIDDEN", "DIRECTIVE_OPEN_PARENS", "DIRECTIVE_CLOSE_PARENS", "DIRECTIVE_BANG", + "DIRECTIVE_OP_EQ", "DIRECTIVE_OP_NE", "DIRECTIVE_OP_AND", "DIRECTIVE_OP_OR", "CONDITIONAL_SYMBOL", + }; + + /// + /// Set of antlr type names for CSharp branch keywords. + /// + private static readonly HashSet cSharpBranchKeywords = new() + { + "FOR", "FOREACH", "IF", "SWITCH", "TRY", "WHILE" + }; + + /// + /// Set of antlr type names for CSharp integer and floating point literals. + /// + private static readonly HashSet cSharpNumbers = new() + { + "LITERAL_ACCESS", "INTEGER_LITERAL", "HEX_INTEGER_LITERAL", "BIN_INTEGER_LITERAL", "REAL_LITERAL", "DIGITS" + }; + + /// Set of antlr type names for CSharp character and string literals. + private static readonly HashSet cSharpStrings = new() + { + "CHARACTER_LITERAL", "REGULAR_STRING", "VERBATIUM_STRING", "INTERPOLATED_REGULAR_STRING_START", + "INTERPOLATED_VERBATIUM_STRING_START", "VERBATIUM_DOUBLE_QUOTE_INSIDE", + "DOUBLE_QUOTE_INSIDE", "REGULAR_STRING_INSIDE", "VERBATIUM_INSIDE_STRING" + }; + + /// Set of antlr type names for CSharp separators and operators. + private static readonly HashSet cSharpPunctuation = new() + { + "OPEN_BRACE", "CLOSE_BRACE", "CLOSE_BRACE_INSIDE", "OPEN_BRACKET", + "CLOSE_BRACKET", "OPEN_PARENS", "CLOSE_PARENS", "DOT", "COMMA", "FORMAT_STRING", "COLON", "SEMICOLON", "PLUS", "MINUS", "STAR", "DIV", + "PERCENT", "AMP", "BITWISE_OR", "CARET", "BANG", "TILDE", "ASSIGNMENT", "LT", "GT", "INTERR", "DOUBLE_COLON", + "OP_COALESCING", "OP_INC", "OP_DEC", "OP_AND", "OP_OR", "OP_PTR", "OP_EQ", "OP_NE", "OP_LE", "OP_GE", "OP_ADD_ASSIGNMENT", + "OP_SUB_ASSIGNMENT", "OP_MULT_ASSIGNMENT", "OP_DIV_ASSIGNMENT", "OP_MOD_ASSIGNMENT", "OP_AND_ASSIGNMENT", "OP_OR_ASSIGNMENT", + "OP_XOR_ASSIGNMENT", "OP_LEFT_SHIFT", "OP_LEFT_SHIFT_ASSIGNMENT", "OP_COALESCING_ASSIGNMENT", "OP_RANGE", + "DOUBLE_CURLY_INSIDE", "OPEN_BRACE_INSIDE", "REGULAR_CHAR_INSIDE" + }; + + /// Set of antlr type names for CSharp identifiers. + private static readonly HashSet cSharpIdentifiers = new() + { + "IDENTIFIER", "TEXT" + }; + + /// + /// Set of antlr type names for CSharp whitespace. + /// + private static readonly HashSet cSharpWhitespace = new() + { + "WHITESPACES", "DIRECTIVE_WHITESPACES" + }; + + /// + /// Set of antlr type names for CSharp newlines. + /// + private static readonly HashSet cSharpNewlines = new() + { + "NL", "TEXT_NEW_LINE", "DIRECTIVE_NEW_LINE" + }; + + /// + /// Set of antlr type names for Java comments. + /// + private static readonly HashSet cSharpComments = new() + { + "SINGLE_LINE_DOC_COMMENT", "DELIMITED_DOC_COMMENT", "SINGLE_LINE_COMMENT", "DELIMITED_COMMENT", + "DIRECTIVE_SINGLE_LINE_COMMENT" + }; + + #endregion + + #region CPP Language + + /// + /// Name of the antlr grammar lexer. + /// + private const string cppFileName = "CPP14Lexer.g4"; + + /// + /// Set of CPP file extensions. + /// + private static readonly HashSet cppExtensions = new() + { + "cpp", "cxx", "hpp" + }; + + /// + /// Set of antlr type names for CPP keywords excluding . + /// + private static readonly HashSet cppKeywords = new() + { + "Alignas", "Alignof", "Asm", "Auto", "Bool", "Break", "Case", "Catch", "Continue", + "Char", "Char16", "Char32", "Class", "Const", "Constexpr", "Const_cast", + "Decltype", "Default", "Delete", "Do", "Double", "Dynamic_cast", "Else", + "Enum", "Explicit", "Export", "Extern", "False_", "Final", "Float", + "Friend", "Goto", "Inline", "Int", "Long", "Mutable", "Namespace", + "New", "Noexcept", "Nullptr", "Operator", "Override", "Private", "Protected", + "Public", "Register", "Reinterpret_cast", "Return", "Short", "Signed", + "Sizeof", "Static", "Static_assert", "Static_cast", "Struct", + "Template", "This", "Thread_local", "Throw", "True_", "Typedef", + "Typeid_", "Typename_", "Union", "Unsigned", "Using", "Virtual", "Void", + "Volatile", "Wchar", + "BooleanLiteral", "PointerLiteral", "UserDefinedLiteral", + "MultiLineMacro", "Directive" + }; + + /// + /// Set of antlr type names for CPP branch keywords. + /// + private static readonly HashSet cppBranchKeywords = new() + { + "For", "If", "Switch", "Try", "While" + }; + + /// + /// Set of antlr type names for CPP integer and floating point literals. + /// + private static readonly HashSet cppNumbers = new() + { + "IntegerLiteral", "FloatingLiteral", "DecimalLiteral", "OctalLiteral", "HexadecimalLiteral", + "BinaryLiteral", "Integersuffix", "UserDefinedIntegerLiteral", "UserDefinedFloatingLiteral" + }; + + /// Set of antlr type names for CPP character and string literals. + private static readonly HashSet cppStrings = new() + { + "StringLiteral", "CharacterLiteral", "UserDefinedStringLiteral", "UserDefinedCharacterLiteral" + }; + + /// Set of antlr type names for CPP separators and operators. + private static readonly HashSet cppPunctuation = new() + { + "LeftParen", "RightParen", "LeftBracket", + "RightBracket", "LeftBrace", "RightBrace", "Plus", "Minus", "Star", "Div", + "Mod", "Caret", "And", "Or", "Tilde", "Not", "Assign", "Less", "Greater", + "PlusAssign", "MinusAssign", "StarAssign", "DivAssign", "ModAssign", "XorAssign", + "AndAssign", "OrAssign", "LeftShiftAssign", "RightShiftAssign", "Equal", + "NotEqual", "LessEqual", "GreaterEqual", "AndAnd", "OrOr", "PlusPlus", + "MinusMinus", "Comma", "ArrowStar", "Arrow", "Question", "Colon", "Doublecolon", + "Semi", "Dot", "DotStar", "Ellipsis" + }; + + /// Set of antlr type names for CPP identifiers. + private static readonly HashSet cppIdentifiers = new() { "Identifier" }; + + /// + /// Set of antlr type names for CPP whitespace. + /// + private static readonly HashSet cppWhitespace = new() { "Whitespace" }; + + /// + /// Set of antlr type names for CPP newlines. + /// + private static readonly HashSet cppNewlines = new() { "Newline" }; + + /// + /// Set of antlr type names for CPP comments. + /// + private static readonly HashSet cppComments = new() { "BlockComment", "LineComment" }; + + #endregion + + #region Plain Text "Language" + + /// + /// Name of the antlr grammar lexer. + /// + private const string plainFileName = "PlainTextLexer.g4"; + + /// + /// Set of plain text file extensions. + /// Note that this is a special case, since this is the lexer we'll use when nothing else is available. + /// + private static readonly HashSet plainExtensions = new(); + + /// Set of antlr type names for keywords. There are none here. + private static readonly HashSet plainKeywords = new(); + + /// Set of antlr type names for branch keywords. There are none here. + private static readonly HashSet plainBranchKeywords = new(); + + /// Set of antlr type names for numbers. + private static readonly HashSet plainNumbers = new(); + + /// Set of antlr type names for character and string literals. There are none here. + private static readonly HashSet plainStrings = new(); + + /// Set of antlr type names for punctuation. + private static readonly HashSet plainPunctuation = new(); + + /// Set of antlr type names for identifiers, which in this case is for normal words. + private static readonly HashSet plainIdentifiers = new() + { + "WORD", "PSEUDOWORD", "LETTERS", "SIGNS", "SPECIAL", "NUMBERS" + }; + + /// Set of antlr type names for whitespace. + private static readonly HashSet plainWhitespace = new() { "WHITESPACES" }; + + /// Set of antlr type names for newlines. + private static readonly HashSet plainNewlines = new() { "NEWLINES" }; + + /// Set of antlr type names for comments. There are none here. + private static readonly HashSet plainComments = new(); + + #endregion + + #region Static Types + + /// + /// Token Language for Java. + /// + public static readonly AntlrLanguage Java = new(javaFileName, javaExtensions, javaKeywords, javaBranchKeywords, javaNumbers, + javaStrings, javaPunctuation, javaIdentifiers, javaWhitespace, javaNewlines, javaComments); + + /// + /// Token Language for C#. + /// + public static readonly AntlrLanguage CSharp = new(cSharpFileName, cSharpExtensions, cSharpKeywords, cSharpBranchKeywords, cSharpNumbers, + cSharpStrings, cSharpPunctuation, cSharpIdentifiers, cSharpWhitespace, cSharpNewlines, cSharpComments); + + /// + /// Token Language for CPP. + /// + public static readonly AntlrLanguage CPP = new(cppFileName, cppExtensions, cppKeywords, cppBranchKeywords, cppNumbers, + cppStrings, cppPunctuation, cppIdentifiers, cppWhitespace, cppNewlines, cppComments); + + /// + /// Token language for plain text. + /// + public static readonly AntlrLanguage Plain = new(plainFileName, plainExtensions, plainKeywords, plainBranchKeywords, plainNumbers, + plainStrings, plainPunctuation, plainIdentifiers, plainWhitespace, plainNewlines, plainComments); + + #endregion + + public static IEnumerable AllAntlrLanguages => AllTokenLanguages.OfType(); + + /// + /// Constructor for the token language. + /// + /// Should never be accessible from outside this class. + /// Name of this lexer grammar + /// List of file extensions for this language + /// Keywords of this language + /// Number literals of this language + /// String literals of this language + /// Punctuation for this language + /// Identifiers for this language + /// Whitespace for this language + /// Newlines for this language + /// Comments for this language + /// Branches for this language + /// Number of spaces a tab is equivalent to + private AntlrLanguage(string lexerFileName, ISet fileExtensions, ISet keywords, ISet branchKeywords, + ISet numberLiterals, ISet stringLiterals, ISet punctuation, + ISet identifiers, ISet whitespace, ISet newline, + ISet comments, int tabWidth = defaultTabWidth) : base(lexerFileName, fileExtensions, tabWidth) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (AllAntlrLanguages.Except(new []{this}).Any(x => x.LexerFileName == lexerFileName || x.FileExtensions.Overlaps(fileExtensions))) + { + throw new ArgumentException("Lexer file name and file extensions must be unique per language!"); + } + if (AnyOverlaps()) + { + throw new ArgumentException("Symbolic names may not appear in more than one set each!"); + } +#endif + LexerFileName = lexerFileName; + Keywords = keywords; + BranchKeywords = branchKeywords; + NumberLiterals = numberLiterals; + StringLiterals = stringLiterals; + Punctuation = punctuation; + Identifiers = identifiers; + Whitespace = whitespace; + Newline = newline; + Comments = comments; + + return; + + // Check whether any of the symbolic names are used twice + bool AnyOverlaps() + { + return keywords.Intersect(numberLiterals).Intersect(stringLiterals).Intersect(punctuation) + .Intersect(identifiers).Intersect(whitespace).Intersect(newline) + .Intersect(comments).Intersect(branchKeywords).Any(); + } + } + + /// + /// Returns the matching token language for the given . + /// If no matching token language is found, an exception will be thrown. + /// + /// File name of the antlr lexer. Can be found in lexer.GrammarFileName + /// The matching token language + /// If the given is not supported. + public static AntlrLanguage FromLexerFileName(string lexerFileName) + { + return AllAntlrLanguages.SingleOrDefault(x => x.LexerFileName.Equals(lexerFileName)) + ?? throw new ArgumentException($"The given {nameof(lexerFileName)} is not of a supported grammar. Supported grammars are " + + string.Join(", ", AllAntlrLanguages.Select(x => x.LexerFileName))); + } + + /// + /// Returns the matching token language for the given . + /// If no matching token language is found, the will be used, unless + /// is true. + /// + /// File extension for the language. + /// + /// Whether to throw an exception when an unknown file extension is encountered. + /// If this is false, the will be used instead in such a case. + /// + /// The matching token language. + /// + /// If the given is not supported and is true. + /// + public static AntlrLanguage FromFileExtension(string extension, bool throwOnUnknown = false) + { + AntlrLanguage target = AllAntlrLanguages.SingleOrDefault(x => x.FileExtensions.Contains(extension)); + if (target == null) + { + if (throwOnUnknown) + { + throw new ArgumentException("The given filetype is not supported in Antlr. Supported filetypes are " + + string.Join(", ", AllAntlrLanguages.SelectMany(x => x.FileExtensions))); + } + + target = Plain; + } + + return target; + } + + + /// + /// Creates a new lexer matching the of this language. + /// + /// The string which shall be parsed by the lexer. + /// the new matching lexer + /// If no lexer is defined for this language. + public Lexer CreateLexer(string content) + { + ICharStream input = CharStreams.fromString(content); + return LexerFileName switch + { + javaFileName => new Java9Lexer(input), + cSharpFileName => new CSharpLexer(input), + cppFileName => new CPP14Lexer(input), + plainFileName => new PlainTextLexer(input), + _ => throw new InvalidOperationException("No lexer defined for this language yet.") + }; + } + + /// + /// Returns the type of token this is. + /// The type of token will be represented by the name of the collection it is in. + /// Returns null if the token is not any known type. + /// + /// a symbolic name from the antlr lexer for this language + /// name of the type the given is, or null if it isn't known. + public string TypeName(string token) + { + // We go through each category and check whether it contains the token. + // I know that this looks like it may be abstracted because the same thing is done on different objects + // in succession, but due to the usage of nameof() a refactoring of this kind would break it. + if (Keywords.Contains(token)) + { + return nameof(Keywords); + } + if (BranchKeywords.Contains(token)) + { + return nameof(BranchKeywords); + } + if (NumberLiterals.Contains(token)) + { + return nameof(NumberLiterals); + } + if (StringLiterals.Contains(token)) + { + return nameof(StringLiterals); + } + if (Punctuation.Contains(token)) + { + return nameof(Punctuation); + } + if (Identifiers.Contains(token)) + { + return nameof(Identifiers); + } + if (Comments.Contains(token)) + { + return nameof(Comments); + } + if (Whitespace.Contains(token)) + { + return nameof(Whitespace); + } + if (Newline.Contains(token)) + { + return nameof(Newline); + } + return eof.Equals(token) ? nameof(eof) : null; + } + } +} diff --git a/Assets/SEE/Scanner/Antlr/AntlrLanguage.cs.meta b/Assets/SEE/Scanner/Antlr/AntlrLanguage.cs.meta new file mode 100644 index 0000000000..544a746015 --- /dev/null +++ b/Assets/SEE/Scanner/Antlr/AntlrLanguage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3ae2a8a900efeb0faaf5430fff524a7b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SEE/Scanner/Antlr/AntlrToken.cs b/Assets/SEE/Scanner/Antlr/AntlrToken.cs new file mode 100644 index 0000000000..e87f068efb --- /dev/null +++ b/Assets/SEE/Scanner/Antlr/AntlrToken.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Antlr4.Runtime; +using Cysharp.Threading.Tasks; + +namespace SEE.Scanner.Antlr +{ + /// + /// Represents a token from a source code file, including , a , + /// and some , emitted by an Antlr lexer. + /// + public record AntlrToken : SEEToken + { + private AntlrToken(string Text, TokenType TokenType, AntlrLanguage Language) : base(Text, TokenType, Language) { } + + /// + /// Creates a new from the given scanned by the given + /// Antlr . + /// + /// The token which shall be converted to an + /// The Antlr lexer with which the token was created. + /// The language of the . + /// If this is not given, the language will be inferred from the given 's grammar. + /// The corresponding to the given . + private static AntlrToken FromAntlrIToken(IToken token, Lexer lexer, AntlrLanguage language = null) + { + return new AntlrToken(token.Text, + AntlrTokenType.FromAntlrType(language, lexer.Vocabulary.GetSymbolicName(token.Type)), + language ?? AntlrLanguage.FromLexerFileName(lexer.GrammarFileName)); + } + + /// + /// Returns a stream of s created by parsing the file at the supplied + /// . + /// + /// Path to the source code file which shall be read and parsed. + /// A list of tokens created from the source code file. + /// + ///
    + ///
  • The language of the file will be determined by checking its file extension.
  • + ///
  • Each token will be created by using .
  • + ///
+ ///
+ public static async UniTask> FromFileAsync(string filePath) + { + AntlrLanguage language = AntlrLanguage.FromFileExtension(Path.GetExtension(filePath)?[1..]); + Lexer lexer = language.CreateLexer(await File.ReadAllTextAsync(filePath)); + CommonTokenStream tokenStream = new(lexer); + tokenStream.Fill(); + // Generate list of SEETokens using the token stream and its language + return tokenStream.GetTokens().Select(x => FromAntlrIToken(x, lexer, language)); + } + + /// + /// Returns a list of s created by parsing the given , assuming + /// it's in the given . + /// + /// Text from which the token stream shall be created. + /// Language the given is written in + /// A list of tokens created from the source code file. + public static IList FromString(string text, AntlrLanguage language) + { + Lexer lexer = language.CreateLexer(text); + CommonTokenStream tokenStream = new(lexer); + tokenStream.Fill(); + // Generate list of SEETokens using the token stream and its language + return tokenStream.GetTokens().Select(x => FromAntlrIToken(x, lexer, language)).ToList(); + } + } +} diff --git a/Assets/SEE/Scanner/Antlr/AntlrToken.cs.meta b/Assets/SEE/Scanner/Antlr/AntlrToken.cs.meta new file mode 100644 index 0000000000..4b48cd099a --- /dev/null +++ b/Assets/SEE/Scanner/Antlr/AntlrToken.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e9af09c72f549db7cae8049266ddd5fb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SEE/Scanner/Antlr/AntlrTokenType.cs b/Assets/SEE/Scanner/Antlr/AntlrTokenType.cs new file mode 100644 index 0000000000..e1a136786c --- /dev/null +++ b/Assets/SEE/Scanner/Antlr/AntlrTokenType.cs @@ -0,0 +1,84 @@ +using System; +using System.Linq; + +namespace SEE.Scanner.Antlr +{ + /// + /// Represents a kind of token in an Antlr-supported programming language, with an associated color. + /// For example, this may be a or an . + /// + public record AntlrTokenType : TokenType + { + /// + /// Returns the corresponding for the given + /// in the given . If it's not recognized, an exception is thrown. + /// + /// The language the is from + /// Symbolic name from an antlr lexer + /// The corresponding token for the given . + /// If or is null + /// If the is not recognized + public static TokenType FromAntlrType(AntlrLanguage language, string symbolicName) + { + if (language == null || symbolicName == null) + { + throw new ArgumentNullException(); + } + + string typeName = language.TypeName(symbolicName); + TokenType type = AllTokens.SingleOrDefault(x => x.Name.Equals(typeName)); + if (type == null) + { + throw new InvalidOperationException($"Unknown token type: {typeName}/{symbolicName}"); + } + return type; + } + + #region Static TokenTypes + + // IMPORTANT: The name has to match with the name of the collection in AntlrLanguage! + + /// + /// Keyword tokens. This also includes boolean literals and null literals. + /// + public static readonly AntlrTokenType Keyword = new("Keywords", "#D988F2"); // purple + + /// + /// Branch keyword tokens. + /// + /// We want s have the same color as + /// other s. + public static readonly AntlrTokenType BranchKeyword = new("BranchKeywords", "#D988F2"); // purple + + /// + /// Number literal tokens. This includes integer literals, floating point literals, etc. + /// + public static readonly AntlrTokenType NumberLiteral = new("NumberLiterals", "#D48F35"); // orange + + /// + /// String literal tokens. This also includes character literals. + /// + public static readonly AntlrTokenType StringLiteral = new("StringLiterals", "#92F288"); // light green + + /// + /// Punctuation tokens, such as separators and operators. + /// + public static readonly AntlrTokenType Punctuation = new("Punctuation", "#96E5FF"); // light blue + + /// + /// Identifier tokens, such as variable names. + /// + public static readonly AntlrTokenType Identifier = new("Identifiers", "#FFFFFF"); // white + + /// + /// Comments of any kind. + /// + public static readonly AntlrTokenType Comment = new("Comments", "#6F708E"); // dark bluish gray + + #endregion + + private AntlrTokenType(string name, string color) : base(name, color) + { + } + } +} diff --git a/Assets/SEE/Scanner/Antlr/AntlrTokenType.cs.meta b/Assets/SEE/Scanner/Antlr/AntlrTokenType.cs.meta new file mode 100644 index 0000000000..0c5bec7bb2 --- /dev/null +++ b/Assets/SEE/Scanner/Antlr/AntlrTokenType.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c8d7797f2fae4e57a275b98721c9df08 +timeCreated: 1719432972 \ No newline at end of file diff --git a/Assets/SEE/Scanner/LSP.meta b/Assets/SEE/Scanner/LSP.meta new file mode 100644 index 0000000000..8dadd5cb51 --- /dev/null +++ b/Assets/SEE/Scanner/LSP.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ed9affb576424a6b95cb933ff3ea0182 +timeCreated: 1719516199 \ No newline at end of file diff --git a/Assets/SEE/Scanner/LSP/LSPToken.cs b/Assets/SEE/Scanner/LSP/LSPToken.cs new file mode 100644 index 0000000000..a7aaae88df --- /dev/null +++ b/Assets/SEE/Scanner/LSP/LSPToken.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Cysharp.Threading.Tasks; +using MoreLinq; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using SEE.Tools.LSP; +using UnityEngine.Assertions; + +namespace SEE.Scanner.LSP +{ + /// + /// Represents a semantic token from a source code file, including , a , + /// and some , emitted by a language server. + /// + public record LSPToken : SEEToken + { + private LSPToken(string Text, TokenType TokenType, TokenModifiers Modifiers, LSPLanguage Language) : base(Text, TokenType, Language, Modifiers) { } + + /// + /// Returns a stream of s created by parsing the file at the supplied + /// . The tokens are generated for the given , + /// using the given LSP . + /// + /// Path to the source code file which shall be read and parsed. + /// The LSP handler used to retrieve the semantic tokens. + /// The language of the file. + /// A stream of tokens created from the source code file. + public static async UniTask> FromFileAsync(string filePath, LSPHandler handler, LSPLanguage language) + { + const int delayMs = 100; + const int maxTries = 30; + + string fileContents = await System.IO.File.ReadAllTextAsync(filePath); + // We may need to wait for the server to process the file, so we just retry for a bit + // (with the above constants, at most 3 seconds) until we get the tokens. + SemanticTokens tokens; + int tries = 0; + do + { + tokens = await handler.GetSemanticTokensAsync(filePath); + if (tokens.Data.Length == 0) + { + await UniTask.Delay(delayMs); + } + } while (tokens.Data.Length == 0 && ++tries < maxTries); + if (handler.ServerCapabilities.SemanticTokensProvider == null) + { + throw new InvalidOperationException("The server does not support semantic tokens."); + } + return FromSemanticTokens(tokens, handler.ServerCapabilities.SemanticTokensProvider.Legend, language, fileContents); + } + + /// + /// Returns a stream of s from the given LSP-generated + /// and , for the given and . + /// + /// The semantic tokens to be converted to s. + /// The legend used to interpret the semantic tokens. + /// The language of the file. + /// The contents of the file the tokens are from as a single string. + /// A stream of tokens created from the semantic tokens. + private static IEnumerable FromSemanticTokens(SemanticTokens tokens, SemanticTokensLegend legend, LSPLanguage language, string fileContents) + { + IList modifierLegend = legend.TokenModifiers.ToList(); + IList typeLegend = legend.TokenTypes.ToList(); + + Assert.IsTrue(tokens.Data.Length % 5 == 0, "Semantic tokens data length must be a multiple of 5."); + + // Both cursors are indices within the fileContents. + // tokenCursor is the cursor for the end of the current LSPToken. + int tokenCursor = 0; + // semanticTokenCursor is the cursor for the start of the current semantic token. + int semanticTokenCursor = 0; + for (int i = 0; i < tokens.Data.Length; i += 5) + { + // For a description of the encoding, see the section on semantic tokens in the LSP specification. + + // Token line number, relative to the previous token. + int deltaLine = tokens.Data[i]; + // Token start character, relative to either 0 or the previous token’s start if they are on the same line + int deltaStart = tokens.Data[i + 1]; + // Length of the token + int length = tokens.Data[i + 2]; + // The type of the token, represented as an index within the typeLegend. + int type = tokens.Data[i + 3]; + // The modifiers of the token, represented as a bitmask of indices within the modifierLegend. + int modifiers = tokens.Data[i + 4]; + + if (deltaLine > 0) + { + // We need to skip ahead until the cursor is at the start of the relevant line + // (depending on deltaLine). + for (int j = 0; j < deltaLine; j++) + { + semanticTokenCursor = fileContents.IndexOf('\n', semanticTokenCursor) + 1; + } + } + + // Then, we need to move the cursor ahead by deltaStart. + semanticTokenCursor += deltaStart; + + // The tokens given by LSP may not encompass the whole document. + // For example, there are no LSP token types for newlines and whitespace. + // Hence, we have to fill these gaps ourselves by constructing tokens for them manually. + if (semanticTokenCursor > tokenCursor) + { + // We now have to differentiate between Newlines, Whitespace, and other tokens. + string gap = fileContents.Substring(tokenCursor, semanticTokenCursor - tokenCursor); + foreach (LSPToken token in HandleGap(gap)) + { + yield return token; + } + } + + tokenCursor = semanticTokenCursor + length; + + string tokenText = fileContents[semanticTokenCursor..tokenCursor]; + LSPTokenType tokenType = LSPTokenType.FromSemanticTokenType(typeLegend[type]); + // Modifiers are a bitmask, so we need to map them to the actual modifiers using the legend. + TokenModifiers tokenModifiers = modifierLegend.Where((_, index) => (modifiers & (1 << index)) != 0) + .Aggregate(TokenModifiers.None, (x, y) => x | y.FromLspTokenModifier()); + yield return new LSPToken(tokenText, tokenType, tokenModifiers, language); + } + + // There may be leftover tokens at the end of the file. + if (tokenCursor < fileContents.Length) + { + string gap = fileContents[tokenCursor..]; + foreach (LSPToken token in HandleGap(gap)) + { + yield return token; + } + } + + yield return new LSPToken(string.Empty, TokenType.EOF, TokenModifiers.None, language); + yield break; + + IEnumerable HandleGap(string gap) + { + // This gap may consist of multiple tokens. + // For example, the string ";\n //" consists of four tokens: + // A semicolon, a newline, four spaces, and two slashes. + return Regex.Split(gap, @"(?<=\S)(?=\s)|(?<=\s)(?=\S)").SelectMany(HandleGapToken); + } + + IEnumerable HandleGapToken(string gapToken) + { + if (string.IsNullOrWhiteSpace(gapToken)) + { + Assert.IsNotNull(gapToken); + // This can still contain newlines, which we need to handle separately. + string[] gapTokens = gapToken.Split('\n'); + foreach (string whitespace in gapTokens.Interleave(MoreEnumerable.Return("\n").Repeat(gapTokens.Length - 1))) + { + if (whitespace == "\n") + { + yield return new LSPToken(whitespace, TokenType.Newline, TokenModifiers.None, language); + } + else + { + yield return new LSPToken(whitespace, TokenType.Whitespace, TokenModifiers.None, language); + } + } + } + else + { + yield return new LSPToken(gapToken, LSPTokenType.Type, TokenModifiers.None, language); + } + } + } + } +} diff --git a/Assets/SEE/Scanner/LSP/LSPToken.cs.meta b/Assets/SEE/Scanner/LSP/LSPToken.cs.meta new file mode 100644 index 0000000000..695ac94b96 --- /dev/null +++ b/Assets/SEE/Scanner/LSP/LSPToken.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b1fdc2c975e8456782f48b3d0090dbbd +timeCreated: 1719516220 \ No newline at end of file diff --git a/Assets/SEE/Scanner/LSP/LSPTokenType.cs b/Assets/SEE/Scanner/LSP/LSPTokenType.cs new file mode 100644 index 0000000000..466dea678a --- /dev/null +++ b/Assets/SEE/Scanner/LSP/LSPTokenType.cs @@ -0,0 +1,170 @@ +using System.Collections.Generic; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace SEE.Scanner.LSP +{ + /// + /// Represents a kind of token in an LSP-supported programming language, with an associated color. + /// For example, this may be a or an . + /// + public record LSPTokenType : TokenType + { + private LSPTokenType(string name, string color) : base(name, color) { } + + /// + /// For identifiers that declare or reference a namespace, module, or package. + /// + public static readonly LSPTokenType Namespace = new("namespace", "#FFCB6B"); + + /// + /// For identifiers that declare or reference a class type. + /// + public static readonly LSPTokenType Class = new("class", "#FFCB6B"); + + /// + /// For identifiers that declare or reference an enumeration type. + /// + public static readonly LSPTokenType Enum = new("enum", "#F78C6C"); + + /// + /// For identifiers that declare or reference an interface type. + /// + public static readonly LSPTokenType Interface = new("interface", "#C3E88D"); + + /// + /// For identifiers that declare or reference a struct type. + /// + public static readonly LSPTokenType Struct = new("struct", "#FFCB6B"); + + /// + /// For identifiers that declare or reference a type parameter. + /// + public static readonly LSPTokenType TypeParameter = new("typeParameter", "#C3E88D"); + + /// + /// For identifiers that declare or reference function or method parameter. + /// + public static readonly LSPTokenType Parameter = new("parameter", "#F78C6C"); + /// + /// For identifiers that declare or reference a local or global variable. + /// + public static readonly LSPTokenType Variable = new("variable", "#EEFFE3"); + /// + /// For identifiers that declare or reference a member property, member field, or member variable. + /// + public static readonly LSPTokenType Property = new("property", "#EEFFFF"); + + /// + /// For identifiers that declare or reference an enumeration property, constant, or member. + /// + public static readonly LSPTokenType EnumMember = new("enumMember", "#F78C6C"); + + /// + /// For identifiers that declare an event property. + /// + public static readonly LSPTokenType Event = new("event", "#EEFFE3"); + + /// + /// For identifiers that declare a function. + /// + public static readonly LSPTokenType Function = new("function", "#82AAFF"); + + /// + /// For identifiers that declare a member function or method. + /// + public static readonly LSPTokenType Method = new("method", "#82AAFF"); + + /// + /// For identifiers that declare a macro. + /// + public static readonly LSPTokenType Macro = new("macro", "#C792EA"); + + /// + /// For tokens that represent a language keyword. + /// + public static readonly LSPTokenType Keyword = new("keyword", "#C792EA"); + + /// + /// For tokens that represent a modifier. + /// + public static readonly LSPTokenType Modifier = new("modifier", "#C792EA"); + + /// + /// For tokens that represent a comment. + /// + public static readonly LSPTokenType Comment = new("comment", "#717CB4"); + + /// + /// For tokens that represent a string literal. + /// + public static readonly LSPTokenType String = new("string", "#C3E88D"); + + /// + /// For tokens that represent a number literal. + /// + public static readonly LSPTokenType Number = new("number", "#F78C6C"); + + /// + /// For tokens that represent a regular expression literal. + /// + public static readonly LSPTokenType Regexp = new("regexp", "#93E88D"); + + /// + /// For tokens that represent an operator. + /// + public static readonly LSPTokenType Operator = new("operator", "#89DDFF"); + + /// + /// For identifiers that declare or reference decorators and annotations. + /// + public static readonly LSPTokenType Decorator = new("decorator", "#FFCB6B"); + + /// + /// For identifiers that declare a label. + /// + public static readonly LSPTokenType Label = new("label", "#C3D3DE"); + + /// + /// Represents a generic type. Acts as a fallback for types which can't be mapped to one of the other types. + /// + public static readonly LSPTokenType Type = new("type", "#FFFFFF"); + + /// + /// A mapping of to . + /// + private static readonly Dictionary semanticTokenMapping = new() + { + { SemanticTokenType.Comment, Comment }, + { SemanticTokenType.Keyword, Keyword }, + { SemanticTokenType.String, String }, + { SemanticTokenType.Number, Number }, + { SemanticTokenType.Regexp, Regexp }, + { SemanticTokenType.Operator, Operator }, + { SemanticTokenType.Namespace, Namespace }, + { SemanticTokenType.Type, Type }, + { SemanticTokenType.Struct, Struct }, + { SemanticTokenType.Class, Class }, + { SemanticTokenType.Interface, Interface }, + { SemanticTokenType.Enum, Enum }, + { SemanticTokenType.TypeParameter, TypeParameter }, + { SemanticTokenType.Function, Function }, + { SemanticTokenType.Method, Method }, + { SemanticTokenType.Property, Property }, + { SemanticTokenType.Macro, Macro }, + { SemanticTokenType.Variable, Variable }, + { SemanticTokenType.Parameter, Parameter }, + { SemanticTokenType.Label, Label }, + { SemanticTokenType.Modifier, Modifier }, + { SemanticTokenType.Event, Event }, + { SemanticTokenType.EnumMember, EnumMember }, + { SemanticTokenType.Decorator, Decorator } + }; + + /// + /// Returns the that corresponds to the given . + /// + /// The to map to an . + /// The that corresponds to the given . + public static LSPTokenType FromSemanticTokenType(SemanticTokenType semanticTokenType) => semanticTokenMapping.GetValueOrDefault(semanticTokenType, Type); + } +} diff --git a/Assets/SEE/Scanner/LSP/LSPTokenType.cs.meta b/Assets/SEE/Scanner/LSP/LSPTokenType.cs.meta new file mode 100644 index 0000000000..6f6eff39a1 --- /dev/null +++ b/Assets/SEE/Scanner/LSP/LSPTokenType.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a07bb9a9e8e24b58bf95f2ea9a7cec74 +timeCreated: 1719516212 \ No newline at end of file diff --git a/Assets/SEE/Scanner/SEEToken.cs b/Assets/SEE/Scanner/SEEToken.cs index c7a035dca7..bf52a934ef 100644 --- a/Assets/SEE/Scanner/SEEToken.cs +++ b/Assets/SEE/Scanner/SEEToken.cs @@ -1,257 +1,12 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Antlr4.Runtime; -using UnityEngine; - -namespace SEE.Scanner +namespace SEE.Scanner { /// - /// Represents a token from a source code file, including and a . + /// Represents a token from a source code file, including , a , + /// and some . /// - public class SEEToken - { - /// - /// The text of the token. - /// - public readonly string Text; - - /// - /// The type of this token. - /// - public readonly Type TokenType; - - /// - /// The inclusive index of the beginning of this token. - /// The index is measured from the beginning of the file, whereas the first character has the index 0. - /// - public readonly int StartOffset; - - /// - /// The exclusive index of the end of this token. - /// In other words, this is the index of the first character not belonging to this token. - /// The index is measured from the beginning of the file, whereas the first character has the index 0. - /// - public readonly int EndOffset; - - /// - /// The language of the source code this token was parsed from. - /// - public readonly TokenLanguage Language; - - /// - /// Constructor for this class. - /// - /// Text of this token. Must not be null. - /// Type of this token. Must not be null. - /// Start offset of this token. Must not be negative, except for the special value - /// -1, which indicates the very first newline of the file - /// End offset of this token. Must not be smaller than . - /// The language of the source code this token is from - /// - /// If or is null. - /// - /// - /// If is smaller than - /// - /// If is less than -1. - public SEEToken(string text, Type type, int startOffset, int endOffset, TokenLanguage language) - { - Text = text ?? throw new ArgumentNullException(nameof(text)); - TokenType = type ?? throw new ArgumentNullException(nameof(type)); - if (endOffset < startOffset) - { - throw new ArgumentOutOfRangeException($"{nameof(endOffset)} must not be smaller than {nameof(startOffset)}!"); - } - // endOffset is greater than startOffset at this point, so it can't be negative after this part - else if (startOffset < -1) - { - throw new ArgumentException($"{nameof(startOffset)} must not be less than -1!"); - } - - StartOffset = startOffset; - EndOffset = endOffset; - Language = language; - } - - /// - /// Creates a new from the given parsed by the given - /// . - /// - /// The token which shall be converted to a - /// The lexer with which the token was created. - /// The language of the . - /// If this is not given, the language will be inferred from the given 's grammar. - /// The corresponding to the given . - public static SEEToken FromAntlrToken(IToken token, Lexer lexer, TokenLanguage language = null) - { - language ??= TokenLanguage.FromLexerFileName(lexer.GrammarFileName); - return new SEEToken(token.Text, - Type.FromAntlrType(language, lexer.Vocabulary.GetSymbolicName(token.Type)), - token.StartIndex, token.StopIndex + 1, language); // Antlr StopIndex is inclusive - } - - /// - /// Returns a list of s created by parsing the file at the supplied - /// . - /// - /// Path to the source code file which shall be read and parsed. - /// A list of tokens created from the source code file. - /// - ///
    - ///
  • The language of the file will be determined by checking its file extension.
  • - ///
  • Each token will be created by using .
  • - ///
- ///
- public static IEnumerable FromFile(string filename) - { - TokenLanguage language = TokenLanguage.FromFileExtension(Path.GetExtension(filename)?[1..]); - Lexer lexer = language.CreateLexer(File.ReadAllText(filename)); - CommonTokenStream tokenStream = new(lexer); - tokenStream.Fill(); - // Generate list of SEETokens using the token stream and its language - return tokenStream.GetTokens().Select(x => FromAntlrToken(x, lexer, language)); - } - - /// - /// Returns a list of s created by parsing the given , assuming - /// it's in the given . - /// - /// Text from which the token stream shall be created. - /// Language the given is written in - /// A list of tokens created from the source code file. - public static IList FromString(string text, TokenLanguage language) - { - Lexer lexer = language.CreateLexer(text); - CommonTokenStream tokenStream = new(lexer); - tokenStream.Fill(); - // Generate list of SEETokens using the token stream and its language - return tokenStream.GetTokens().Select(x => FromAntlrToken(x, lexer, language)).ToList(); - } - - /// - /// Represents a kind of token in a programming language, with an associated color. - /// For example, this may be a or an . - /// - public class Type - { - /// - /// Name of the token type. - /// This has to match with the corresponding collection in . - /// - public string Name { get; } - - /// - /// Color the token type should be rendered in in hexadecimal RGB notation (no '#' sign). - /// An optional fourth byte may be entered to define the alpha value. - /// - /// Red would be "FF0000". Semitransparent black would be "00000088". - public string Color { get; } - - #region Static Types - - /// - /// A list of all possible tokens. - /// - public static IList AllTokens { get; } = new List(); - - // IMPORTANT: The name has to match with the name of the collection in TokenLanguage! - - /// - /// Keyword tokens. This also includes boolean literals and null literals. - /// - public static readonly Type Keyword = new("Keywords", "D988F2"); // purple - - /// - /// Branch keyword tokens. - /// - /// We want s have the same color as - /// other s. - public static readonly Type BranchKeyword = new("BranchKeywords", "D988F2"); // purple - - /// - /// Number literal tokens. This includes integer literals, floating point literals, etc. - /// - public static readonly Type NumberLiteral = new("NumberLiterals", "D48F35"); // orange - - /// - /// String literal tokens. This also includes character literals. - /// - public static readonly Type StringLiteral = new("StringLiterals", "92F288"); // light green - - /// - /// Punctuation tokens, such as separators and operators. - /// - public static readonly Type Punctuation = new("Punctuation", "96E5FF"); // light blue - - /// - /// Identifier tokens, such as variable names. - /// - public static readonly Type Identifier = new("Identifiers", "FFFFFF"); // white - - /// - /// Comments of any kind. - /// - public static readonly Type Comment = new("Comments", "6F708E"); // dark bluish gray - - /// - /// Whitespace tokens, excluding newlines. - /// - public static readonly Type Whitespace = new("Whitespace", "000000"); // color doesn't matter - - /// - /// Newline tokens. Must contain exactly one newline. - /// - public static readonly Type Newline = new("Newline", "000000"); // color doesn't matter - - /// - /// End-Of-File token. - /// - public static readonly Type EOF = new("eof", "000000"); // color doesn't matter - - /// - /// Unknown tokens, i.e. those not recognized by the lexer. - /// - public static readonly Type Unknown = new("Unknown", "FFFFFF"); // white - - #endregion - - /// - /// Constructor for this class. - /// - /// Must never be accessible from the outside. - /// Name of this token type - /// Color this token type should be shown in - private Type(string name, string color) - { - Color = color; - Name = name; - AllTokens.Add(this); - } - - /// - /// Returns the corresponding for the given in the given - /// . If it's not recognized, will be returned. - /// - /// The language the is from - /// Symbolic name from an antlr lexer - /// The corresponding token for the given . - public static Type FromAntlrType(TokenLanguage language, string symbolicName) - { - if (language == null || symbolicName == null) - { - throw new ArgumentNullException(); - } - - string typeName = language.TypeName(symbolicName); - Type type = AllTokens.SingleOrDefault(x => x.Name.Equals(typeName)); - if (type == null) - { - Debug.LogError($"Unknown token type: {typeName}/{symbolicName}"); - } - return type ?? Unknown; - } - } - } + /// The text of the token. + /// The type of the token (e.g., class). + /// The language of the token. + /// The modifiers of the token (e.g., static. + public abstract record SEEToken(string Text, TokenType TokenType, TokenLanguage Language, TokenModifiers Modifiers = TokenModifiers.None); } diff --git a/Assets/SEE/Scanner/TokenLanguage.cs b/Assets/SEE/Scanner/TokenLanguage.cs index 82be4b2cdd..d9a963b5a9 100644 --- a/Assets/SEE/Scanner/TokenLanguage.cs +++ b/Assets/SEE/Scanner/TokenLanguage.cs @@ -1,36 +1,26 @@ -using System; using System.Collections.Generic; -using System.Linq; -using Antlr4.Runtime; namespace SEE.Scanner { /// - /// Represents a language a is in. - /// Symbolic names for the antlr lexer are specified here. + /// A programming language a is in. /// - public class TokenLanguage + public abstract class TokenLanguage { /// /// Default number of spaces a tab is equivalent to. /// - private const int defaultTabWidth = 4; + protected const int defaultTabWidth = 4; /// /// Language-independent symbolic name for the end of file token. /// - private const string eof = "EOF"; + protected const string eof = "EOF"; /// - /// File extensions which apply for the given language. - /// May not intersect any other languages file extensions. + /// The name of the language. /// - public ISet FileExtensions { get; } - - /// - /// Name of the antlr lexer file the keywords were taken from. - /// - public string LexerFileName { get; } + public string Name { get; } /// /// Number of spaces equivalent to a tab in this language. @@ -39,570 +29,22 @@ public class TokenLanguage public int TabWidth { get; } /// - /// Symbolic names for comments of a language, including block, line, and documentation comments. - /// - public ISet Comments { get; } - - /// - /// Symbolic names for keywords of a language. This also includes boolean literals and null literals. - /// - public ISet Keywords { get; } - - /// - /// Symbolic names for branch keywords of a language. - /// - public ISet BranchKeywords { get; } - - /// - /// Symbolic names for number literals of a language. This includes integer literals, floating point literals, etc. - /// - public ISet NumberLiterals { get; } - - /// - /// Symbolic names for string literals of a language. Also includes character literals. - /// - public ISet StringLiterals { get; } - - /// - /// Symbolic names for separators and operators of a language. - /// - public ISet Punctuation { get; } - - /// - /// Symbolic names for identifiers in a language. - /// - public ISet Identifiers { get; } - - /// - /// Symbolic names for whitespace in a language, excluding newlines. - /// - public ISet Whitespace { get; } - - /// - /// Symbolic names for newlines in a language. - /// - public ISet Newline { get; } - - #region Java Language - - /// - /// Name of the Java antlr grammar lexer. - /// - private const string javaFileName = "Java9Lexer.g4"; - - /// - /// Set of java file extensions. - /// - private static readonly HashSet javaExtensions = new() - { - "java" - }; - - /// - /// Set of antlr type names for Java keywords excluding . - /// - private static readonly HashSet javaKeywords = new() - { - "ABSTRACT", "ASSERT", "BOOLEAN", "BREAK", "BYTE", "CASE", "CATCH", "CHAR", "CLASS", "CONST", "CONTINUE", - "DEFAULT", "DO", "DOUBLE", "ELSE", "ENUM", "EXPORTS", "EXTENDS", "FINAL", "FINALLY", "FLOAT", - "GOTO", "IMPLEMENTS", "IMPORT", "INSTANCEOF", "INT", "INTERFACE", "LONG", "MODULE", "NATIVE", "NEW", - "OPEN", "OPERNS", "PACKAGE", "PRIVATE", "PROTECTED", "PROVIDES", "PUBLIC", "REQUIRES", "RETURN", "SHORT", - "STATIC", "STRICTFP", "SUPER", "SYNCHRONIZED", "THIS", "THROW", "THROWS", "TO", "TRANSIENT", - "TRANSITIVE", "USES", "VOID", "VOLATILE", "WITH", "UNDER_SCORE", - "BooleanLiteral", "NullLiteral" - }; - - /// - /// Set of antlr type names for Java branch keywords. - /// - private static readonly HashSet javaBranchKeywords = new() - { - "FOR", "IF", "SWITCH", "TRY", "WHILE" - }; - - /// - /// Set of antlr type names for Java integer and floating point literals. - /// - private static readonly HashSet javaNumbers = new() { "IntegerLiteral", "FloatingPointLiteral" }; - - /// Set of antlr type names for Java character and string literals. - private static readonly HashSet javaStrings = new() { "CharacterLiteral", "StringLiteral" }; - - /// Set of antlr type names for Java separators and operators. - private static readonly HashSet javaPunctuation = new() - { - "LPAREN", "RPAREN", "LBRACE", - "RBRACE", "LBRACK", "RBRACK", "SEMI", "COMMA", "DOT", "ELLIPSIS", "AT", "COLONCOLON", - "ASSIGN", "GT", "LT", "BANG", "TILDE", "QUESTION", "COLON", "ARROW", "EQUAL", "LE", "GE", "NOTEQUAL", "AND", - "OR", "INC", "DEC", "ADD", "SUB", "MUL", "DIV", "BITAND", "BITOR", "CARET", "MOD", - "ADD_ASSIGN", "SUB_ASSIGN", "MUL_ASSIGN", "DIV_ASSIGN", "AND_ASSIGN", "OR_ASSIGN", "XOR_ASSIGN", - "MOD_ASSIGN", "LSHIFT_ASSIGN", "RSHIFT_ASSIGN", "URSHIFT_ASSIGN" - }; - - /// Set of antlr type names for Java identifiers. - private static readonly HashSet javaIdentifiers = new() { "Identifier" }; - - /// - /// Set of antlr type names for Java whitespace. - /// - private static readonly HashSet javaWhitespace = new() { "WS" }; - - /// - /// Set of antlr type names for Java newlines. - /// - private static readonly HashSet javaNewlines = new() { "NEWLINE" }; - - /// - /// Set of antlr type names for Java comments. - /// - private static readonly HashSet javaComments = new() { "COMMENT", "LINE_COMMENT" }; - - #endregion - - #region C# Language - - /// - /// Name of the C# antlr grammar lexer. - /// - private const string cSharpFileName = "CSharpLexer.g4"; - - /// - /// Set of CSharp file extensions. - /// - private static readonly HashSet cSharpExtensions = new() - { - "cs" - }; - - /// - /// Set of antlr type names for CSharp keywords excluding . - /// - private static readonly HashSet cSharpKeywords = new() - { - // General keywords - "ABSTRACT", "ADD", "ALIAS", "ARGLIST", "AS", "ASCENDING", "ASYNC", "AWAIT", "BASE", "BOOL", "BREAK", "BY", - "BYTE", "CASE", "CATCH", "CHAR", "CHECKED", "CLASS", "CONST", "CONTINUE", "DECIMAL", "DEFAULT", "DELEGATE", - "DESCENDING", "DO", "DOUBLE", "DYNAMIC", "ELSE", "ENUM", "EQUALS", "EVENT", "EXPLICIT", "EXTERN", "FALSE", - "FINALLY", "FIXED", "FLOAT", "FROM", "GET", "GOTO", "GROUP", "IMPLICIT", "IN", "INT", - "INTERFACE", "INTERNAL", "INTO", "IS", "JOIN", "LET", "LOCK", "LONG", "NAMEOF", "NAMESPACE", "NEW", "NULL_", - "OBJECT", "ON", "OPERATOR", "ORDERBY", "OUT", "OVERRIDE", "PARAMS", "PARTIAL", "PRIVATE", "PROTECTED", - "PUBLIC", "READONLY", "REF", "REMOVE", "RETURN", "SBYTE", "SEALED", "SELECT", "SET", "SHORT", "SIZEOF", - "STACKALLOC", "STATIC", "STRING", "STRUCT", "THIS", "THROW", "TRUE", "TYPEOF", "UINT", - "ULONG", "UNCHECKED", "UNMANAGED", "UNSAFE", "USHORT", "USING", "VAR", "VIRTUAL", "VOID", "VOLATILE", "WHEN", - "WHERE", "YIELD", "SHARP", - // Directive keywords (anything within a directive is treated as a keyword, similar to IDEs - "DIRECTIVE_TRUE", "DIRECTIVE_FALSE", "DEFINE", "UNDEF", "DIRECTIVE_IF", - "ELIF", "DIRECTIVE_ELSE", "ENDIF", "LINE", "ERROR", "WARNING", "REGION", "ENDREGION", "PRAGMA", "NULLABLE", - "DIRECTIVE_DEFAULT", "DIRECTIVE_HIDDEN", "DIRECTIVE_OPEN_PARENS", "DIRECTIVE_CLOSE_PARENS", "DIRECTIVE_BANG", - "DIRECTIVE_OP_EQ", "DIRECTIVE_OP_NE", "DIRECTIVE_OP_AND", "DIRECTIVE_OP_OR", "CONDITIONAL_SYMBOL", - }; - - /// - /// Set of antlr type names for CSharp branch keywords. - /// - private static readonly HashSet cSharpBranchKeywords = new() - { - "FOR", "FOREACH", "IF", "SWITCH", "TRY", "WHILE" - }; - - /// - /// Set of antlr type names for CSharp integer and floating point literals. - /// - private static readonly HashSet cSharpNumbers = new() - { - "LITERAL_ACCESS", "INTEGER_LITERAL", "HEX_INTEGER_LITERAL", "BIN_INTEGER_LITERAL", "REAL_LITERAL", "DIGITS" - }; - - /// Set of antlr type names for CSharp character and string literals. - private static readonly HashSet cSharpStrings = new() - { - "CHARACTER_LITERAL", "REGULAR_STRING", "VERBATIUM_STRING", "INTERPOLATED_REGULAR_STRING_START", - "INTERPOLATED_VERBATIUM_STRING_START", "VERBATIUM_DOUBLE_QUOTE_INSIDE", - "DOUBLE_QUOTE_INSIDE", "REGULAR_STRING_INSIDE", "VERBATIUM_INSIDE_STRING" - }; - - /// Set of antlr type names for CSharp separators and operators. - private static readonly HashSet cSharpPunctuation = new() - { - "OPEN_BRACE", "CLOSE_BRACE", "CLOSE_BRACE_INSIDE", "OPEN_BRACKET", - "CLOSE_BRACKET", "OPEN_PARENS", "CLOSE_PARENS", "DOT", "COMMA", "FORMAT_STRING", "COLON", "SEMICOLON", "PLUS", "MINUS", "STAR", "DIV", - "PERCENT", "AMP", "BITWISE_OR", "CARET", "BANG", "TILDE", "ASSIGNMENT", "LT", "GT", "INTERR", "DOUBLE_COLON", - "OP_COALESCING", "OP_INC", "OP_DEC", "OP_AND", "OP_OR", "OP_PTR", "OP_EQ", "OP_NE", "OP_LE", "OP_GE", "OP_ADD_ASSIGNMENT", - "OP_SUB_ASSIGNMENT", "OP_MULT_ASSIGNMENT", "OP_DIV_ASSIGNMENT", "OP_MOD_ASSIGNMENT", "OP_AND_ASSIGNMENT", "OP_OR_ASSIGNMENT", - "OP_XOR_ASSIGNMENT", "OP_LEFT_SHIFT", "OP_LEFT_SHIFT_ASSIGNMENT", "OP_COALESCING_ASSIGNMENT", "OP_RANGE", - "DOUBLE_CURLY_INSIDE", "OPEN_BRACE_INSIDE", "REGULAR_CHAR_INSIDE" - }; - - /// Set of antlr type names for CSharp identifiers. - private static readonly HashSet cSharpIdentifiers = new() - { - "IDENTIFIER", "TEXT" - }; - - /// - /// Set of antlr type names for CSharp whitespace. - /// - private static readonly HashSet cSharpWhitespace = new() - { - "WHITESPACES", "DIRECTIVE_WHITESPACES" - }; - - /// - /// Set of antlr type names for CSharp newlines. - /// - private static readonly HashSet cSharpNewlines = new() - { - "NL", "TEXT_NEW_LINE", "DIRECTIVE_NEW_LINE" - }; - - /// - /// Set of antlr type names for Java comments. - /// - private static readonly HashSet cSharpComments = new() - { - "SINGLE_LINE_DOC_COMMENT", "DELIMITED_DOC_COMMENT", "SINGLE_LINE_COMMENT", "DELIMITED_COMMENT", - "DIRECTIVE_SINGLE_LINE_COMMENT" - }; - - #endregion - - #region CPP Language - - /// - /// Name of the antlr grammar lexer. - /// - private const string cppFileName = "CPP14Lexer.g4"; - - /// - /// Set of CPP file extensions. - /// - private static readonly HashSet cppExtensions = new() - { - "cpp", "cxx", "hpp" - }; - - /// - /// Set of antlr type names for CPP keywords excluding . - /// - private static readonly HashSet cppKeywords = new() - { - "Alignas", "Alignof", "Asm", "Auto", "Bool", "Break", "Case", "Catch", "Continue", - "Char", "Char16", "Char32", "Class", "Const", "Constexpr", "Const_cast", - "Decltype", "Default", "Delete", "Do", "Double", "Dynamic_cast", "Else", - "Enum", "Explicit", "Export", "Extern", "False_", "Final", "Float", - "Friend", "Goto", "Inline", "Int", "Long", "Mutable", "Namespace", - "New", "Noexcept", "Nullptr", "Operator", "Override", "Private", "Protected", - "Public", "Register", "Reinterpret_cast", "Return", "Short", "Signed", - "Sizeof", "Static", "Static_assert", "Static_cast", "Struct", - "Template", "This", "Thread_local", "Throw", "True_", "Typedef", - "Typeid_", "Typename_", "Union", "Unsigned", "Using", "Virtual", "Void", - "Volatile", "Wchar", - "BooleanLiteral", "PointerLiteral", "UserDefinedLiteral", - "MultiLineMacro", "Directive" - }; - - /// - /// Set of antlr type names for CPP branch keywords. - /// - private static readonly HashSet cppBranchKeywords = new() - { - "For", "If", "Switch", "Try", "While" - }; - - /// - /// Set of antlr type names for CPP integer and floating point literals. - /// - private static readonly HashSet cppNumbers = new() - { - "IntegerLiteral", "FloatingLiteral", "DecimalLiteral", "OctalLiteral", "HexadecimalLiteral", - "BinaryLiteral", "Integersuffix", "UserDefinedIntegerLiteral", "UserDefinedFloatingLiteral" - }; - - /// Set of antlr type names for CPP character and string literals. - private static readonly HashSet cppStrings = new() - { - "StringLiteral", "CharacterLiteral", "UserDefinedStringLiteral", "UserDefinedCharacterLiteral" - }; - - /// Set of antlr type names for CPP separators and operators. - private static readonly HashSet cppPunctuation = new() - { - "LeftParen", "RightParen", "LeftBracket", - "RightBracket", "LeftBrace", "RightBrace", "Plus", "Minus", "Star", "Div", - "Mod", "Caret", "And", "Or", "Tilde", "Not", "Assign", "Less", "Greater", - "PlusAssign", "MinusAssign", "StarAssign", "DivAssign", "ModAssign", "XorAssign", - "AndAssign", "OrAssign", "LeftShiftAssign", "RightShiftAssign", "Equal", - "NotEqual", "LessEqual", "GreaterEqual", "AndAnd", "OrOr", "PlusPlus", - "MinusMinus", "Comma", "ArrowStar", "Arrow", "Question", "Colon", "Doublecolon", - "Semi", "Dot", "DotStar", "Ellipsis" - }; - - /// Set of antlr type names for CPP identifiers. - private static readonly HashSet cppIdentifiers = new() { "Identifier" }; - - /// - /// Set of antlr type names for CPP whitespace. - /// - private static readonly HashSet cppWhitespace = new() { "Whitespace" }; - - /// - /// Set of antlr type names for CPP newlines. - /// - private static readonly HashSet cppNewlines = new() { "Newline" }; - - /// - /// Set of antlr type names for CPP comments. - /// - private static readonly HashSet cppComments = new() { "BlockComment", "LineComment" }; - - #endregion - - #region Plain Text "Language" - - /// - /// Name of the antlr grammar lexer. - /// - private const string plainFileName = "PlainTextLexer.g4"; - - /// - /// Set of plain text file extensions. - /// Note that this is a special case, since this is the lexer we'll use when nothing else is available. + /// File extensions which apply for the given language. + /// May not intersect any other languages file extensions. /// - private static readonly HashSet plainExtensions = new(); - - /// Set of antlr type names for keywords. There are none here. - private static readonly HashSet plainKeywords = new(); - - /// Set of antlr type names for branch keywords. There are none here. - private static readonly HashSet plainBranchKeywords = new(); - - /// Set of antlr type names for numbers. - private static readonly HashSet plainNumbers = new(); - - /// Set of antlr type names for character and string literals. There are none here. - private static readonly HashSet plainStrings = new(); - - /// Set of antlr type names for punctuation. - private static readonly HashSet plainPunctuation = new(); - - /// Set of antlr type names for identifiers, which in this case is for normal words. - private static readonly HashSet plainIdentifiers = new() - { - "WORD", "PSEUDOWORD", "LETTERS", "SIGNS", "SPECIAL", "NUMBERS" - }; - - /// Set of antlr type names for whitespace. - private static readonly HashSet plainWhitespace = new() { "WHITESPACES" }; - - /// Set of antlr type names for newlines. - private static readonly HashSet plainNewlines = new() { "NEWLINES" }; - - /// Set of antlr type names for comments. There are none here. - private static readonly HashSet plainComments = new(); - - #endregion - - #region Static Types + public ISet FileExtensions { get; } /// /// A list of all token languages there are. /// - public static readonly IList AllTokenLanguages = new List(); - - /// - /// Token Language for Java. - /// - public static readonly TokenLanguage Java = new(javaFileName, javaExtensions, javaKeywords, javaBranchKeywords, javaNumbers, - javaStrings, javaPunctuation, javaIdentifiers, javaWhitespace, javaNewlines, javaComments); + public static readonly ISet AllTokenLanguages = new HashSet(); - /// - /// Token Language for C#. - /// - public static readonly TokenLanguage CSharp = new(cSharpFileName, cSharpExtensions, cSharpKeywords, cSharpBranchKeywords, cSharpNumbers, - cSharpStrings, cSharpPunctuation, cSharpIdentifiers, cSharpWhitespace, cSharpNewlines, cSharpComments); - - /// - /// Token Language for CPP. - /// - public static readonly TokenLanguage CPP = new(cppFileName, cppExtensions, cppKeywords, cppBranchKeywords, cppNumbers, - cppStrings, cppPunctuation, cppIdentifiers, cppWhitespace, cppNewlines, cppComments); - - /// - /// Token language for plain text. - /// - public static readonly TokenLanguage Plain = new(plainFileName, plainExtensions, plainKeywords, plainBranchKeywords, plainNumbers, - plainStrings, plainPunctuation, plainIdentifiers, plainWhitespace, plainNewlines, plainComments); - - #endregion - - /// - /// Constructor for the token language. - /// - /// Should never be accessible from outside this class. - /// Name of this lexer grammar - /// List of file extensions for this language - /// Keywords of this language - /// Number literals of this language - /// String literals of this language - /// Punctuation for this language - /// Identifiers for this language - /// Whitespace for this language - /// Newlines for this language - /// Comments for this language - /// Branches for this language - /// Number of spaces a tab is equivalent to - private TokenLanguage(string lexerFileName, ISet fileExtensions, ISet keywords, ISet branchKeywords, - ISet numberLiterals, ISet stringLiterals, ISet punctuation, - ISet identifiers, ISet whitespace, ISet newline, - ISet comments, int tabWidth = defaultTabWidth) + protected TokenLanguage(string name, ISet fileExtensions, int tabWidth = defaultTabWidth) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR - if (AllTokenLanguages.Any(x => x.LexerFileName.Equals(lexerFileName) || x.FileExtensions.Overlaps(fileExtensions))) - { - throw new ArgumentException("Lexer file name and file extensions must be unique per language!"); - } - if (AnyOverlaps()) - { - throw new ArgumentException("Symbolic names may not appear in more than one set each!"); - } -#endif - LexerFileName = lexerFileName; - FileExtensions = fileExtensions; - Keywords = keywords; - BranchKeywords = branchKeywords; - NumberLiterals = numberLiterals; - StringLiterals = stringLiterals; - Punctuation = punctuation; - Identifiers = identifiers; - Whitespace = whitespace; - Newline = newline; - Comments = comments; + Name = name; TabWidth = tabWidth; - + FileExtensions = fileExtensions; AllTokenLanguages.Add(this); - - // Check whether any of the symbolic names are used twice - bool AnyOverlaps() - { - return keywords.Intersect(numberLiterals).Intersect(stringLiterals).Intersect(punctuation) - .Intersect(identifiers).Intersect(whitespace).Intersect(newline) - .Intersect(comments).Intersect(branchKeywords).Any(); - } - } - - /// - /// Returns the matching token language for the given . - /// If no matching token language is found, an exception will be thrown. - /// - /// File name of the antlr lexer. Can be found in lexer.GrammarFileName - /// The matching token language - /// If the given is not supported. - public static TokenLanguage FromLexerFileName(string lexerFileName) - { - return AllTokenLanguages.SingleOrDefault(x => x.LexerFileName.Equals(lexerFileName)) - ?? throw new ArgumentException($"The given {nameof(lexerFileName)} is not of a supported grammar. Supported grammars are " - + string.Join(", ", AllTokenLanguages.Select(x => x.LexerFileName))); - } - - /// - /// Returns the matching token language for the given . - /// If no matching token language is found, the will be used, unless - /// is true. - /// - /// File extension for the language. - /// - /// Whether to throw an exception when an unknown file extension is encountered. - /// If this is false, the will be used instead in such a case. - /// - /// The matching token language. - /// - /// If the given is not supported and is true. - /// - public static TokenLanguage FromFileExtension(string extension, bool throwOnUnknown = false) - { - TokenLanguage target = AllTokenLanguages.SingleOrDefault(x => x.FileExtensions.Contains(extension)); - if (target == null) - { - if (throwOnUnknown) - { - throw new ArgumentException("The given filetype is not supported. Supported filetypes are " - + string.Join(", ", AllTokenLanguages.SelectMany(x => x.FileExtensions))); - } - - target = Plain; - } - - return target; - } - - /// - /// Creates a new lexer matching the of this language. - /// - /// The string which shall be parsed by the lexer. - /// the new matching lexer - /// If no lexer is defined for this language. - public Lexer CreateLexer(string content) - { - ICharStream input = CharStreams.fromString(content); - return LexerFileName switch - { - javaFileName => new Java9Lexer(input), - cSharpFileName => new CSharpLexer(input), - cppFileName => new CPP14Lexer(input), - plainFileName => new PlainTextLexer(input), - _ => throw new InvalidOperationException("No lexer defined for this language yet.") - }; - } - - /// - /// Returns the type of token this is. - /// The type of token will be represented by the name of the collection it is in. - /// Returns null if the token is not any known type. - /// - /// a symbolic name from the antlr lexer for this language - /// name of the type the given is, or null if it isn't known. - public string TypeName(string token) - { - // We go through each category and check whether it contains the token. - // I know that this looks like it may be abstracted because the same thing is done on different objects - // in succession, but due to the usage of nameof() a refactoring of this kind would break it. - if (Keywords.Contains(token)) - { - return nameof(Keywords); - } - if (BranchKeywords.Contains(token)) - { - return nameof(BranchKeywords); - } - if (NumberLiterals.Contains(token)) - { - return nameof(NumberLiterals); - } - if (StringLiterals.Contains(token)) - { - return nameof(StringLiterals); - } - if (Punctuation.Contains(token)) - { - return nameof(Punctuation); - } - if (Identifiers.Contains(token)) - { - return nameof(Identifiers); - } - if (Comments.Contains(token)) - { - return nameof(Comments); - } - if (Whitespace.Contains(token)) - { - return nameof(Whitespace); - } - if (Newline.Contains(token)) - { - return nameof(Newline); - } - return eof.Equals(token) ? nameof(eof) : null; } } } diff --git a/Assets/SEE/Scanner/TokenLanguage.cs.meta b/Assets/SEE/Scanner/TokenLanguage.cs.meta index 15189a1054..39e810c628 100644 --- a/Assets/SEE/Scanner/TokenLanguage.cs.meta +++ b/Assets/SEE/Scanner/TokenLanguage.cs.meta @@ -1,3 +1,3 @@ -fileFormatVersion: 2 -guid: 097994b6864d45cdb3146b812d865ae5 -timeCreated: 1620241512 \ No newline at end of file +fileFormatVersion: 2 +guid: cfe4319940e64ab8b2426e1dc3ef4d7c +timeCreated: 1719433094 \ No newline at end of file diff --git a/Assets/SEE/Scanner/TokenMetrics.cs b/Assets/SEE/Scanner/TokenMetrics.cs index 505f2715e6..c9bef5f57f 100644 --- a/Assets/SEE/Scanner/TokenMetrics.cs +++ b/Assets/SEE/Scanner/TokenMetrics.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using SEE.Scanner.Antlr; using UnityEngine; namespace SEE.Scanner @@ -14,12 +15,12 @@ public static class TokenMetrics /// /// The tokens used for which the complexity should be calculated. /// Returns the McCabe cyclomatic complexity. - public static int CalculateMcCabeComplexity(IEnumerable tokens) + public static int CalculateMcCabeComplexity(IEnumerable tokens) { int complexity = 1; // Starting complexity for a single method or function. // Count decision points (branches). - complexity += tokens.Count(t => t.TokenType == SEEToken.Type.BranchKeyword); + complexity += tokens.Count(t => t.TokenType == AntlrTokenType.BranchKeyword); return complexity; } @@ -59,33 +60,33 @@ float NumberOfDeliveredBugs /// /// The tokens for which the metrics should be calculated. /// Returns the Halstead metrics. - public static HalsteadMetrics CalculateHalsteadMetrics(IEnumerable tokens) + public static HalsteadMetrics CalculateHalsteadMetrics(ICollection tokens) { // Set of token types which are operands. - HashSet operandTypes = new() + HashSet operandTypes = new() { - SEEToken.Type.Identifier, - SEEToken.Type.Keyword, - SEEToken.Type.BranchKeyword, - SEEToken.Type.NumberLiteral, - SEEToken.Type.StringLiteral + AntlrTokenType.Identifier, + AntlrTokenType.Keyword, + AntlrTokenType.BranchKeyword, + AntlrTokenType.NumberLiteral, + AntlrTokenType.StringLiteral }; // Identify operands. HashSet operands = new(tokens.Where(t => operandTypes.Contains(t.TokenType)).Select(t => t.Text)); // Identify operators. - HashSet operators = new(tokens.Where(t => t.TokenType == SEEToken.Type.Punctuation).Select(t => t.Text)); + HashSet operators = new(tokens.Where(t => t.TokenType == AntlrTokenType.Punctuation).Select(t => t.Text)); // Count the total number of operands and operators. int totalOperands = tokens.Count(t => operandTypes.Contains(t.TokenType)); - int totalOperators = tokens.Count(t => t.TokenType == SEEToken.Type.Punctuation); + int totalOperators = tokens.Count(t => t.TokenType == AntlrTokenType.Punctuation); // Derivative Halstead metrics. int programVocabulary = operators.Count + operands.Count; int programLength = totalOperators + totalOperands; - float estimatedProgramLength = operators.Count == 0 ? 0 : (float)((operators.Count * Mathf.Log(operators.Count, 2) + operands.Count * Mathf.Log(operands.Count, 2))); - float volume = programVocabulary == 0 ? 0 : (float)(programLength * Mathf.Log(programVocabulary, 2)); + float estimatedProgramLength = operators.Count == 0 ? 0 : operators.Count * Mathf.Log(operators.Count, 2) + operands.Count * Mathf.Log(operands.Count, 2); + float volume = programVocabulary == 0 ? 0 : programLength * Mathf.Log(programVocabulary, 2); float difficulty = operands.Count == 0 ? 0 : operators.Count / 2.0f * (totalOperands / (float)operands.Count); float effort = difficulty * volume; float timeRequiredToProgram = effort / 18.0f; // Formula: Time T = effort E / S, where S = Stroud's number of psychological 'moments' per second; typically a figure of 18 is used in Software Science. @@ -112,25 +113,25 @@ public static HalsteadMetrics CalculateHalsteadMetrics(IEnumerable tok ///
/// The tokens for which the lines of code should be counted. /// Returns the number of lines of code. - public static int CalculateLinesOfCode(IEnumerable tokens) + public static int CalculateLinesOfCode(IEnumerable tokens) { int linesOfCode = 0; bool comment = false; - foreach (SEEToken token in tokens) + foreach (AntlrToken token in tokens) { - if (token.TokenType == SEEToken.Type.Newline) + if (token.TokenType == TokenType.Newline) { if (!comment) { linesOfCode++; } } - else if (token.TokenType == SEEToken.Type.Comment) + else if (token.TokenType == AntlrTokenType.Comment) { comment = true; } - else if (token.TokenType != SEEToken.Type.Whitespace) + else if (token.TokenType != TokenType.Whitespace) { comment = false; } @@ -138,4 +139,4 @@ public static int CalculateLinesOfCode(IEnumerable tokens) return linesOfCode; } } -} \ No newline at end of file +} diff --git a/Assets/SEE/Scanner/TokenModifiers.cs b/Assets/SEE/Scanner/TokenModifiers.cs new file mode 100644 index 0000000000..5ab837fbe6 --- /dev/null +++ b/Assets/SEE/Scanner/TokenModifiers.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using SEE.Utils; + +namespace SEE.Scanner +{ + /// + /// Modifiers that can be applied to a semantic token + /// (for example, or ). + /// + [Flags] + public enum TokenModifiers + { + /// + /// No modifiers. + /// + None = 0, + + /// + /// For declarations of symbols. + /// + Declaration = 1 << 0, + + /// + /// For definitions of symbols, for example, in header files. + /// + Definition = 1 << 1, + + /// + /// For readonly variables and member fields, as well as constants. + /// + Readonly = 1 << 2, + + /// + /// For class members that are static. + /// + Static = 1 << 3, + + /// + /// For symbols that should no longer be used. + /// + Deprecated = 1 << 4, + + /// + /// For types and member functions that are abstract. + /// + Abstract = 1 << 5, + + /// + /// For functions that are marked as asynchronous. + /// + Async = 1 << 6, + + /// + /// For variable references where the variable is reassigned. + /// + Modification = 1 << 7, + + /// + /// For occurrences of symbols in documentation. + /// + Documentation = 1 << 8, + + /// + /// For symbols that are part of the standard library. + /// + DefaultLibrary = 1 << 9 + } + + /// + /// Extension methods for . + /// + public static class TokenModifiersExtensions + { + /// + /// Mapping from to . + /// + private static readonly IDictionary tokenModifierMapping = new Dictionary + { + { SemanticTokenModifier.Declaration, TokenModifiers.Declaration }, + { SemanticTokenModifier.Definition, TokenModifiers.Definition }, + { SemanticTokenModifier.Readonly, TokenModifiers.Readonly }, + { SemanticTokenModifier.Static, TokenModifiers.Static }, + { SemanticTokenModifier.Deprecated, TokenModifiers.Deprecated }, + { SemanticTokenModifier.Abstract, TokenModifiers.Abstract }, + { SemanticTokenModifier.Async, TokenModifiers.Async }, + { SemanticTokenModifier.Modification, TokenModifiers.Modification }, + { SemanticTokenModifier.Documentation, TokenModifiers.Documentation }, + { SemanticTokenModifier.DefaultLibrary, TokenModifiers.DefaultLibrary } + }; + + /// + /// Converts a to a . + /// + /// The to convert. + /// The that corresponds to the given . + public static TokenModifiers FromLspTokenModifier(this SemanticTokenModifier modifier) + { + return tokenModifierMapping.GetValueOrDefault(modifier); + } + + /// + /// Converts a to the name of a tag that can be used in + /// TextMeshPro's rich text markup. + /// + /// The to convert. Should be a single flag. + /// The name of a tag that can be used in TextMeshPro's rich text markup. + public static string ToRichTextTag(this TokenModifiers modifiers) + { + return modifiers switch + { + TokenModifiers.Static => "i", + TokenModifiers.Deprecated => "strikethrough", + TokenModifiers.Modification => "u", + TokenModifiers.Documentation => "i", + _ => string.Empty + }; + } + + /// + /// Returns a stream of that are set in the given . + /// + /// The to get the set modifiers from. + /// An enumerable of that are set in the given . + public static IEnumerable AsEnumerable(this TokenModifiers modifiers) + { + return Enum.GetValues(typeof(TokenModifiers)).Cast().Where(modifier => modifiers.HasFlag(modifier)); + } + } +} diff --git a/Assets/SEE/Scanner/TokenModifiers.cs.meta b/Assets/SEE/Scanner/TokenModifiers.cs.meta new file mode 100644 index 0000000000..ed2958c94e --- /dev/null +++ b/Assets/SEE/Scanner/TokenModifiers.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 37ae1a4897954349b570caad73383d4f +timeCreated: 1719516779 \ No newline at end of file diff --git a/Assets/SEE/Scanner/TokenType.cs b/Assets/SEE/Scanner/TokenType.cs new file mode 100644 index 0000000000..d743b9a987 --- /dev/null +++ b/Assets/SEE/Scanner/TokenType.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; + +namespace SEE.Scanner +{ + /// + /// Represents a kind of token in a programming language, with an associated color. + /// + public record TokenType + { + #region Static TokenTypes + + /// + /// List of all token types. + /// + protected static readonly IList AllTokens = new List(); + + /// + /// Whitespace tokens, excluding newlines. + /// + public static readonly TokenType Whitespace = new("Whitespace", "#000000"); // color doesn't matter + + /// + /// Newline tokens. Must contain exactly one newline. + /// + public static readonly TokenType Newline = new("Newline", "#000000"); // color doesn't matter + + /// + /// End-Of-File token. + /// + public static readonly TokenType EOF = new("eof", "#000000"); // color doesn't matter + + #endregion + + protected TokenType(string name, string color) + { + Name = name; + Color = color.TrimStart('#'); + + AllTokens.Add(this); + } + + /// Name of the token type. + public string Name { get; } + + /// + /// Color the token type should be rendered in hexadecimal RGB notation (no '#' sign). + /// An optional fourth byte may be entered to define the alpha value. + /// + public string Color { get; } + } +} diff --git a/Assets/SEE/Scanner/TokenType.cs.meta b/Assets/SEE/Scanner/TokenType.cs.meta new file mode 100644 index 0000000000..3f5c0ae2d1 --- /dev/null +++ b/Assets/SEE/Scanner/TokenType.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 27b3e2c6cd244e0baa0aa0f826b6f4bf +timeCreated: 1719426272 \ No newline at end of file diff --git a/Assets/SEE/Tools/LSP/LSPHandler.cs b/Assets/SEE/Tools/LSP/LSPHandler.cs index e9164a389d..2155182cc2 100644 --- a/Assets/SEE/Tools/LSP/LSPHandler.cs +++ b/Assets/SEE/Tools/LSP/LSPHandler.cs @@ -76,7 +76,13 @@ public LSPServer Server /// Whether to log the communication between the language server and SEE to a temporary file. /// [field: SerializeField, HideInInspector] - public bool LogLSP { get; set; } + public bool LogLSP { get; set; } = true; + + /// + /// Whether to use LSP capabilities in code windows. + /// + [field: SerializeField, HideInInspector] + public bool UseInCodeWindows { get; set; } /// /// The language client that is used to communicate with the language server. @@ -160,7 +166,62 @@ public LSPServer Server LabelSupport = false }, Diagnostic = new DiagnosticClientCapabilities(), - PublishDiagnostics = new PublishDiagnosticsCapability() + PublishDiagnostics = new PublishDiagnosticsCapability(), + SemanticTokens = new SemanticTokensCapability() + { + Requests = new SemanticTokensCapabilityRequests() + { + Full = new Supports() + }, + Formats = new[] + { + SemanticTokenFormat.Relative + }, + TokenModifiers = new[] + { + SemanticTokenModifier.Deprecated, + SemanticTokenModifier.Static, + SemanticTokenModifier.Abstract, + SemanticTokenModifier.Readonly, + SemanticTokenModifier.Async, + SemanticTokenModifier.Declaration, + SemanticTokenModifier.Definition, + SemanticTokenModifier.Documentation, + SemanticTokenModifier.Modification, + SemanticTokenModifier.DefaultLibrary + }, + TokenTypes = new[] + { + SemanticTokenType.Comment, + SemanticTokenType.Keyword, + SemanticTokenType.String, + SemanticTokenType.Number, + SemanticTokenType.Regexp, + SemanticTokenType.Operator, + SemanticTokenType.Namespace, + SemanticTokenType.Type, + SemanticTokenType.Struct, + SemanticTokenType.Class, + SemanticTokenType.Interface, + SemanticTokenType.Enum, + SemanticTokenType.TypeParameter, + SemanticTokenType.Function, + SemanticTokenType.Method, + SemanticTokenType.Property, + SemanticTokenType.Macro, + SemanticTokenType.Variable, + SemanticTokenType.Parameter, + SemanticTokenType.Label, + SemanticTokenType.Modifier, + SemanticTokenType.Event, + SemanticTokenType.EnumMember, + SemanticTokenType.Decorator + }, + OverlappingTokenSupport = false, + MultilineTokenSupport = false, + ServerCancelSupport = false, + AugmentsSyntaxTokens = false + } }, Window = new WindowClientCapabilities { @@ -305,12 +366,12 @@ void HandleInitialWorkDoneProgress(WorkDoneProgressCreateParams progressParams) } } - async UniTaskVoid MonitorInitialWorkDoneProgress(ProgressToken token) + async UniTaskVoid MonitorInitialWorkDoneProgress(ProgressToken progressToken) { - await foreach (WorkDoneProgress _ in Client.WorkDoneManager.Monitor(token).ToUniTaskAsyncEnumerable() + await foreach (WorkDoneProgress _ in Client.WorkDoneManager.Monitor(progressToken).ToUniTaskAsyncEnumerable() .Where(x => x.Kind == WorkDoneProgressKind.End)) { - initialWork.Remove(token); + initialWork.Remove(progressToken); } } } @@ -638,6 +699,22 @@ private IUniTaskAsyncEnumerable GetLocationsByLspFunc(string path, int return AsyncUtils.ObserveUntilTimeout(t => lspFunction(parameters, t), TimeoutSpan); } + /// + /// Retrieves semantic tokens for the document at the given . + /// + /// Note that the returned semantic tokens may be empty if the document has not been fully analyzed yet. + /// + /// The path to the document. + /// The semantic tokens for the document at the given path. + public async UniTask GetSemanticTokensAsync(string path) + { + SemanticTokensParams parameters = new() + { + TextDocument = new TextDocumentIdentifier(path) + }; + return await Client.RequestSemanticTokensFull(parameters); + } + /// /// Shuts down the language server and exits its process. /// diff --git a/Assets/SEE/Tools/LSP/LSPLanguage.cs b/Assets/SEE/Tools/LSP/LSPLanguage.cs index f38bdb7e53..53e79be483 100644 --- a/Assets/SEE/Tools/LSP/LSPLanguage.cs +++ b/Assets/SEE/Tools/LSP/LSPLanguage.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using SEE.Scanner; namespace SEE.Tools.LSP { @@ -8,18 +9,8 @@ namespace SEE.Tools.LSP /// A programming language supported by a language server. /// /// - public record LSPLanguage + public class LSPLanguage: TokenLanguage { - /// - /// The name of the language. - /// - public string Name { get; } - - /// - /// The file extensions associated with this language. - /// - public ISet Extensions { get; } - /// /// A mapping from file extensions to LSP language IDs. /// @@ -33,16 +24,13 @@ public record LSPLanguage /// The name of the language. /// The file extensions associated with this language. /// A mapping from file extensions to LSP language IDs. - private LSPLanguage(string name, ISet extensions, IDictionary languageIds = null) + private LSPLanguage(string name, ISet extensions, IDictionary languageIds = null): base(name, extensions) { if (name.Contains('/')) { throw new ArgumentException("Language name must not contain slashes!"); } - Name = name; - Extensions = extensions; LanguageIds = languageIds ?? new Dictionary(); - All.Add(this); } /// @@ -63,7 +51,7 @@ private LSPLanguage(string name, ISet extensions, string languageId) : t /// The language with the given . public static LSPLanguage GetByName(string name) { - return All.First(language => language.Name == name); + return AllLspLanguages.First(language => language.Name == name); } public override string ToString() @@ -71,10 +59,6 @@ public override string ToString() return Name; } - // NOTE: All servers below have been tested first. Before adding a language server to this list, - // please make sure that it actually works in SEE, since we have some special requirements - // (e.g., we require a documentSymbol provider that returns hierarchic `DocumentSymbol` objects). - public static readonly IList All = new List(); public static readonly LSPLanguage C = new("C", new HashSet { "c", "h" }, "c"); public static readonly LSPLanguage CPP = new("C++", new HashSet { @@ -111,5 +95,10 @@ public override string ToString() }, "typescript"); public static readonly LSPLanguage XML = new("XML", new HashSet { "xml", "gxl" }, "xml"); public static readonly LSPLanguage Zig = new("Zig", new HashSet { "zig" }, "zig"); + + /// + /// A list of all supported LSP languages. + /// + public static readonly IList AllLspLanguages = AllTokenLanguages.OfType().ToList(); } } diff --git a/Assets/SEE/Tools/LSP/LSPServer.cs b/Assets/SEE/Tools/LSP/LSPServer.cs index 7928b5f81e..9081b8a500 100644 --- a/Assets/SEE/Tools/LSP/LSPServer.cs +++ b/Assets/SEE/Tools/LSP/LSPServer.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using SEE.Utils; +using static SEE.Tools.LSP.LSPLanguage; namespace SEE.Tools.LSP { @@ -75,7 +76,7 @@ private LSPServer(string name, string websiteURL, IList languages, /// The language ID for the given file . public string LanguageIdFor(string extension) { - LSPLanguage language = Languages.FirstOrDefault(language => language.Extensions.Contains(extension)); + LSPLanguage language = Languages.FirstOrDefault(language => language.FileExtensions.Contains(extension)); return language?.LanguageIds.GetValueOrDefault(extension, language.LanguageIds.GetValueOrDefault(string.Empty)); } @@ -86,69 +87,73 @@ public override string ToString() public static readonly IList All = new List(); + // NOTE: All servers below have been tested first. Before adding a language server to this list, + // please make sure that it actually works in SEE, since we have some special requirements + // (e.g., we require a documentSymbol provider that returns hierarchic `DocumentSymbol` objects). + public static readonly LSPServer Clangd = new("clangd", "https://clangd.llvm.org/", - new List { LSPLanguage.C, LSPLanguage.CPP }, + new List { C, CPP }, "clangd", "--background-index"); public static readonly LSPServer Gopls = new("gopls", "https://github.com/golang/tools/tree/master/gopls", - new List { LSPLanguage.Go }, + new List { Go }, "gopls"); public static readonly LSPServer HaskellLanguageServer = new("Haskell Language Server", "https://haskell-language-server.readthedocs.io/en/latest/", - new List { LSPLanguage.Haskell }, + new List { Haskell }, "haskell-language-server", "--lsp"); public static readonly LSPServer EclipseJdtls = new("Eclipse JDT Language Server", "https://github.com/eclipse-jdtls/eclipse.jdt.ls", - new List { LSPLanguage.Java }, + new List { Java }, "jdtls"); public static readonly LSPServer TypescriptLanguageServer = new("Typescript Language Server", "https://github.com/typescript-language-server/typescript-language-server", - new List { LSPLanguage.TypeScript, LSPLanguage.JavaScript }, + new List { TypeScript, JavaScript }, "typescript-language-server", "--stdio"); public static readonly LSPServer VscodeJson = new("VSCode JSON Language Server", "https://www.npmjs.com/package/vscode-json-languageserver", - new List { LSPLanguage.JSON }, + new List { JSON }, "vscode-json-languageserver", "--stdio"); public static readonly LSPServer Texlab = new("Texlab", "https://github.com/latex-lsp/texlab", - new List { LSPLanguage.LaTeX }, + new List { LaTeX }, "texlab"); public static readonly LSPServer LuaLanguageServer = new("Lua Language Server", "https://github.com/LuaLS/lua-language-server", - new List { LSPLanguage.Lua }, + new List { Lua }, "lua-language-server"); public static readonly LSPServer Marksman = new("Marksman", "https://github.com/artempyanykh/marksman", - new List { LSPLanguage.Markdown }, + new List { Markdown }, "marksman", "server"); public static readonly LSPServer MatlabLanguageServer = new("Matlab Language Server", "https://github.com/mathworks/MATLAB-language-server", - new List { LSPLanguage.MATLAB }, + new List { MATLAB }, "matlab-language-server", "--stdio"); public static readonly LSPServer PhpActor = new("Phpactor", "https://github.com/phpactor/phpactor", - new List { LSPLanguage.PHP }, + new List { PHP }, "phpactor", "language-server"); public static readonly LSPServer Intelephense = new("Intelephense", "https://github.com/bmewburn/vscode-intelephense", - new List { LSPLanguage.PHP }, + new List { PHP }, "intelephense", "--stdio"); public static readonly LSPServer Omnisharp = new("Omnisharp", "https://github.com/OmniSharp/omnisharp-roslyn", - new List { LSPLanguage.CSharp }, + new List { CSharp }, "omnisharp", "-z DotNet:enablePackageRestore=false -e utf-8 -lsp", initOptions: new Dictionary { @@ -164,33 +169,33 @@ public override string ToString() public static readonly LSPServer DartAnalysisServer = new("Dart analysis server", "https://github.com/dart-lang/sdk/blob/master/pkg/analysis_server/tool/lsp_spec/README.md", - new List { LSPLanguage.Dart }, + new List { Dart }, "dart", "language-server"); public static readonly LSPServer KotlinLanguageServer = new("Kotlin Language Server", "https://github.com/fwcd/kotlin-language-server", - new List { LSPLanguage.Kotlin }, + new List { Kotlin }, "kotlin-language-server"); public static readonly LSPServer Pyright = new("Pyright", "https://github.com/microsoft/pyright", - new List { LSPLanguage.Python }, + new List { Python }, "pyright-langserver", "--stdio"); public static readonly LSPServer Jedi = new("Jedi Language Server", "https://github.com/pappasam/jedi-language-server", - new List { LSPLanguage.Python }, + new List { Python }, "jedi-language-server"); public static readonly LSPServer RubyLsp = new("Ruby LSP", "https://github.com/Shopify/ruby-lsp", - new List { LSPLanguage.Ruby }, + new List { Ruby }, "srb", "typecheck --lsp --disable-watchman ."); public static readonly LSPServer RustAnalyzer = new("Rust Analyzer", "https://github.com/rust-lang/rust-analyzer", - new List { LSPLanguage.Rust }, + new List { Rust }, "rust-analyzer", initOptions: new Dictionary { @@ -204,12 +209,12 @@ public override string ToString() public static readonly LSPServer Lemminx = new("Lemminx", "https://github.com/eclipse/lemminx", - new List { LSPLanguage.XML }, + new List { XML }, "lemminx"); public static readonly LSPServer ZLS = new("ZLS", "https://github.com/zigtools/zls", - new List { LSPLanguage.Zig }, + new List { Zig }, "zls"); /// diff --git a/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSessionCodePosition.cs b/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSessionCodePosition.cs index afaabbb88a..0e1fa44116 100644 --- a/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSessionCodePosition.cs +++ b/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSessionCodePosition.cs @@ -6,6 +6,7 @@ using SEE.Utils; using System.IO; using System.Linq; +using Cysharp.Threading.Tasks; using StackFrame = Microsoft.VisualStudio.Shared.VSCodeDebugProtocol.Messages.StackFrame; namespace SEE.UI.DebugAdapterProtocol @@ -87,7 +88,7 @@ private void ShowCodePosition(bool makeActive = false, bool scroll = false, floa { codeWindow = Canvas.AddComponent(); codeWindow.Title = Path.GetFileName(lastCodePath); - codeWindow.EnterFromFile(lastCodePath); + codeWindow.EnterFromFileAsync(lastCodePath).Forget(); manager.AddWindow(codeWindow); codeWindow.OnComponentInitialized += Mark; codeWindow.OnComponentInitialized += MakeActive; diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs b/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs index 3c65aa0b09..ca25e12cfd 100644 --- a/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs @@ -1,6 +1,8 @@ using System; using System.Linq; +using Cysharp.Threading.Tasks; using DG.Tweening; +using SEE.Tools.LSP; using SEE.Utils; using TMPro; using UnityEngine; @@ -52,6 +54,13 @@ public partial class CodeWindow : BaseWindow /// private int lines; + /// + /// The LSP handler for this code window. + /// + /// Will only be set if the LSP feature is enabled and active for this code window. + /// + private LSPHandler lspHandler; + /// /// Path to the code window content prefab. /// @@ -225,17 +234,17 @@ protected override void InitializeFromValueObject(WindowValues valueObject) if (codeValues.Path != null) { - EnterFromFile(codeValues.Path); + EnterFromFileAsync(codeValues.Path).ContinueWith(() => ScrolledVisibleLine = codeValues.VisibleLine).Forget(); } else if (codeValues.Text != null) { EnterFromText(codeValues.Text.Split('\n')); + ScrolledVisibleLine = codeValues.VisibleLine; } else { throw new ArgumentException("Invalid value object. Either FilePath or Text must not be null."); } - ScrolledVisibleLine = codeValues.VisibleLine; } public override void UpdateFromNetworkValueObject(WindowValues valueObject) diff --git a/Assets/SEE/UI/Window/CodeWindow/Input/CodeWindowInput.cs b/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs similarity index 82% rename from Assets/SEE/UI/Window/CodeWindow/Input/CodeWindowInput.cs rename to Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs index 4a4946f85b..bd278cac61 100644 --- a/Assets/SEE/UI/Window/CodeWindow/Input/CodeWindowInput.cs +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs @@ -11,6 +11,9 @@ using SEE.Net.Dashboard; using SEE.Net.Dashboard.Model.Issues; using SEE.Scanner; +using SEE.Scanner.Antlr; +using SEE.Scanner.LSP; +using SEE.Tools.LSP; using SEE.Utils; using UnityEngine; using UnityEngine.Assertions; @@ -68,8 +71,7 @@ public void EnterFromTokens(IEnumerable tokens, // Avoid multiple enumeration in case iteration over the data source is expensive. tokenList = tokens.ToList(); - TokenLanguage language = tokenList.FirstOrDefault()?.Language; - if (language == null) + if (!tokenList.Any()) { text = "This file is empty."; return; @@ -77,8 +79,8 @@ public void EnterFromTokens(IEnumerable tokens, // Unsurprisingly, each newline token corresponds to a new line. // However, we need to also add "hidden" newlines contained in other tokens, e.g. block comments. - int assumedLines = tokenList.Count(x => x.TokenType.Equals(SEEToken.Type.Newline)) - + tokenList.Where(x => !x.TokenType.Equals(SEEToken.Type.Newline)) + int assumedLines = tokenList.Count(x => x.TokenType.Equals(TokenType.Newline)) + + tokenList.Where(x => !x.TokenType.Equals(TokenType.Newline)) .Aggregate(0, (_, token) => token.Text.Count(x => x == '\n')); // Needed padding is the number of lines, because the line number will be at most this long. neededPadding = assumedLines.ToString().Length; @@ -90,16 +92,11 @@ public void EnterFromTokens(IEnumerable tokens, foreach (SEEToken token in tokenList) { - if (token.TokenType == SEEToken.Type.Unknown) - { - Debug.LogError($"Unknown token encountered for text '{token.Text}'.\n"); - } - - if (token.TokenType == SEEToken.Type.Newline) + if (token.TokenType == TokenType.Newline) { AppendNewline(ref lineNumber, ref text, neededPadding, token); } - else if (token.TokenType != SEEToken.Type.EOF) // Skip EOF token completely. + else if (token.TokenType != TokenType.EOF) // Skip EOF token completely. { lineNumber = HandleToken(token); } @@ -183,15 +180,28 @@ int HandleToken(SEEToken token) } } - if (token.TokenType == SEEToken.Type.Whitespace) + if (token.TokenType == TokenType.Whitespace) { // We just copy the whitespace verbatim, no need to even color it. // Note: We have to assume that whitespace will not interfere with TMP's XML syntax. - text += line.Replace("\t", new string(' ', language.TabWidth)); + text += line.Replace("\t", new string(' ', token.Language.TabWidth)); } else { - text += $"{line.Replace("/noparse", "")}"; + List tags = token.Modifiers.AsEnumerable().Select(x => x.ToRichTextTag()) + .Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList(); + foreach (string textTag in tags) + { + text += $"<{textTag}>"; + } + text += $""; + text += $"{line.Replace("/noparse", "")}"; + text += ""; + tags.Reverse(); + foreach (string textTag in tags) + { + text += $""; + } } // Close any potential issue marking @@ -220,7 +230,7 @@ bool HandleContentBasedIssue(int theLineNumber, SEEToken currentToken) // to determine whether the entity will arrive in this line or not. IList lineTokens = tokenList.SkipWhile(x => x != currentToken).Skip(1) - .TakeWhile(x => x.TokenType != SEEToken.Type.Newline + .TakeWhile(x => x.TokenType != TokenType.Newline && !x.Text.Intersect(newlineCharacters).Any()).ToList(); string line = lineTokens.Aggregate("", (s, t) => s + t.Text); MatchCollection matches = Regex.Matches(line, Regex.Escape(entityContent)); @@ -326,14 +336,12 @@ public void EnterFromText(string[] text, bool asIs = false) } /// - /// Populates the code window with the contents of the given file. + /// Populates the code window with the syntax-highlighted contents of the given file. /// This will overwrite any existing text. + /// Syntax-highlighting will be done using the LSP, or Antlr if LSP is not configured for this code city. /// /// The platform-specific filename for the file to read. - /// Whether syntax highlighting shall be enabled. - /// The language will be detected by looking at the file extension. - /// If the language is not supported, an ArgumentException will be thrown. - public void EnterFromFile(string filename, bool syntaxHighlighting = true) + public async UniTask EnterFromFileAsync(string filename) { FilePath = filename; @@ -345,41 +353,68 @@ public void EnterFromFile(string filename, bool syntaxHighlighting = true) return; } - try + text = "Loading code window text..."; + lines = 1; + + // TODO (#250): Maybe disable syntax highlighting for huge files, as it may impact performance badly. + using (LoadingSpinner.ShowIndeterminate($"Loading {Path.GetFileName(filename)}...")) { - // TODO (#250): Maybe disable syntax highlighting for huge files, as it may impact performance badly. - if (syntaxHighlighting) + GameObject go = SceneQueries.GetCodeCity(transform).gameObject; + IEnumerable tokens; + try { - try + // Usage of LSP in code windows must be configured in the LSPHandler, + // the language server must support semantic tokens, and the language of the file + // (inferred from the file extension) must be supported by the server. + if (go.TryGetComponent(out lspHandler) && lspHandler.UseInCodeWindows + && lspHandler.ServerCapabilities.SemanticTokensProvider != null + && TryGetLanguageOrLog(lspHandler, out LSPLanguage language)) { - EnterFromTokens(SEEToken.FromFile(filename)); - GameObject go = SceneQueries.GetCodeCity(transform)?.gameObject; - if (go && go.TryGetComponentOrLog(out AbstractSEECity city) - && city.ErosionSettings.ShowIssuesInCodeWindow) - { - MarkIssuesAsync(filename).Forget(); // initiate issue search - } - else if (HasStarted) - { - textMesh.SetText(text); - SetupBreakpoints(); - } + lspHandler.enabled = true; + lspHandler.OpenDocument(filename); + tokens = await LSPToken.FromFileAsync(filename, lspHandler, language); } - catch (ArgumentException e) + else { - // In case the filetype is not supported, we render the text normally. - Debug.LogError($"Encountered an exception, disabling syntax highlighting: {e}"); + lspHandler = null; + tokens = await AntlrToken.FromFileAsync(filename); } } - else + catch (IOException exception) { - EnterFromText(File.ReadAllLines(filename)); + ShowNotification.Error("File access error", $"Couldn't access file {filename}: {exception}"); + Destroyer.Destroy(this); + return; + } + EnterFromTokens(tokens); + + if (HasStarted) + { + textMesh.SetText(text); + await UniTask.Yield(); // Wait one frame for the text meshes to be updated. + SetupBreakpoints(); + } + + if (go.TryGetComponentOrLog(out AbstractSEECity city) && city.ErosionSettings.ShowIssuesInCodeWindow) + { + MarkIssuesAsync(filename).Forget(); // initiate issue search in background } } - catch (IOException exception) + return; + + // Returns true iff the language for the given filename is supported by the LSP server. + bool TryGetLanguageOrLog(LSPHandler handler, out LSPLanguage language) { - ShowNotification.Error("File access error", $"Couldn't access file {filename}: {exception}"); - Destroyer.Destroy(this); + string extension = Path.GetExtension(filename).TrimStart('.'); + language = handler.Server.Languages.FirstOrDefault(x => x.FileExtensions.Contains(extension)); + if (language == null) + { + ShowNotification.Warn("Unsupported LSP language", + $"Language for extension '{extension}' not supported by the configured LSP server. " + + "Falling back to Antlr for syntax highlighting, LSP capabilities will not be available."); + return false; + } + return true; } } diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs.meta b/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs.meta new file mode 100644 index 0000000000..24a41d9ed3 --- /dev/null +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 24e6c0c71bc5d636e9ca95845011d464 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs b/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs index a6135556c1..7ad349916d 100644 --- a/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs +++ b/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs @@ -13,7 +13,6 @@ using UnityEngine.UI; using System.Collections.Generic; using Microsoft.VisualStudio.Shared.VSCodeDebugProtocol.Messages; -using Michsky.UI.ModernUIPack; using SEE.Controls; using SEE.UI.DebugAdapterProtocol; @@ -223,6 +222,10 @@ private void OnDestroy() { DebugBreakpointManager.OnBreakpointAdded -= OnBreakpointAdded; DebugBreakpointManager.OnBreakpointRemoved -= OnBreakpointRemoved; + if (lspHandler != null) + { + lspHandler.CloseDocument(FilePath); + } } /// diff --git a/Assets/SEE/UI/Window/CodeWindow/Input.meta b/Assets/SEE/UI/Window/CodeWindow/Input.meta deleted file mode 100644 index 7ea5e880cd..0000000000 --- a/Assets/SEE/UI/Window/CodeWindow/Input.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 1f4f6dbe830d43b3bf6104cb865b58a2 -timeCreated: 1622912774 \ No newline at end of file diff --git a/Assets/SEE/UI/Window/CodeWindow/Input/CodeWindowInput.cs.meta b/Assets/SEE/UI/Window/CodeWindow/Input/CodeWindowInput.cs.meta deleted file mode 100644 index 1fd5dcedc9..0000000000 --- a/Assets/SEE/UI/Window/CodeWindow/Input/CodeWindowInput.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 405cfed1cbb0445e910e9556d3277c5d -timeCreated: 1622912874 \ No newline at end of file diff --git a/Assets/SEE/Utils/AsyncUtils.cs b/Assets/SEE/Utils/AsyncUtils.cs index 0758fa9b6e..1b710687e0 100644 --- a/Assets/SEE/Utils/AsyncUtils.cs +++ b/Assets/SEE/Utils/AsyncUtils.cs @@ -53,7 +53,7 @@ public static async UniTask RunWithTimeoutAsync(Func tokens = SEEToken.FromString(code, TokenLanguage.CSharp); + IEnumerable tokens = AntlrToken.FromString(code, AntlrLanguage.CSharp); int complexity = TokenMetrics.CalculateMcCabeComplexity(tokens); Assert.AreEqual(expected, complexity); } @@ -59,7 +60,7 @@ public void TestCalculateHalsteadMetrics() // Test case for empty code, in case DistinctOperators, DistinctOperands and/or ProgramVocabulary values are zero. string emptyCode = ""; - IEnumerable tokensEmptyCode = SEEToken.FromString(emptyCode, TokenLanguage.Plain); + IList tokensEmptyCode = AntlrToken.FromString(emptyCode, AntlrLanguage.Plain); TokenMetrics.HalsteadMetrics expectedEmptyCode = new(DistinctOperators: 0, DistinctOperands: 0, TotalOperators: 0, @@ -85,7 +86,7 @@ public static void main(String[] args) { } }"; - IEnumerable tokens = SEEToken.FromString(code, TokenLanguage.Java); + IList tokens = AntlrToken.FromString(code, AntlrLanguage.Java); TokenMetrics.HalsteadMetrics expected = new(DistinctOperators: 11, DistinctOperands: 16, TotalOperators: 17, @@ -116,7 +117,7 @@ public static void main(String[] args) { // Test case for code with no operators to test Plain Text. string codeWithNoOperators = "This arbitary file has no code.\nJust plain words."; // "." is its own operand. - IEnumerable tokensNoOperators = SEEToken.FromString(codeWithNoOperators, TokenLanguage.Plain); + IList tokensNoOperators = AntlrToken.FromString(codeWithNoOperators, AntlrLanguage.Plain); TokenMetrics.HalsteadMetrics expectedNoOperators = new(DistinctOperators: 0, DistinctOperands: 10, TotalOperators: 0, @@ -164,7 +165,7 @@ void setX(int y) { [TestCase(" ", 0)] public void TestCalculateLinesOfCode(string code, int expected) { - IEnumerable tokens = SEEToken.FromString(code, TokenLanguage.CPP); + IEnumerable tokens = AntlrToken.FromString(code, AntlrLanguage.CPP); int linesOfCode = TokenMetrics.CalculateLinesOfCode(tokens); Assert.AreEqual(expected, linesOfCode); } From fc548acc0b4ee5b77c1588a53d03b92e5c0fc4d8 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Wed, 17 Jul 2024 21:48:25 +0200 Subject: [PATCH 02/23] Add ReferenceEqualityComparer from .NET 5.x source code This can be used to override equality checks to use reference equality, which can be useful for e.g. records. --- Assets/SEE/Utils/ReferenceEqualityComparer.cs | 53 +++++++++++++++++++ .../Utils/ReferenceEqualityComparer.cs.meta | 3 ++ 2 files changed, 56 insertions(+) create mode 100644 Assets/SEE/Utils/ReferenceEqualityComparer.cs create mode 100644 Assets/SEE/Utils/ReferenceEqualityComparer.cs.meta diff --git a/Assets/SEE/Utils/ReferenceEqualityComparer.cs b/Assets/SEE/Utils/ReferenceEqualityComparer.cs new file mode 100644 index 0000000000..440c9f6a79 --- /dev/null +++ b/Assets/SEE/Utils/ReferenceEqualityComparer.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace SEE.Utils +{ + // NOTE: The below class was copied from the .NET 5.x source code. + // Unity uses .NET 4.x, so this class would otherwise not be available. + + /// + /// An that uses reference equality () + /// instead of value equality () when comparing two object instances. + /// + /// + /// The type cannot be instantiated. Instead, use the property + /// to access the singleton instance of this type. + /// + public sealed class ReferenceEqualityComparer : IEqualityComparer + { + private ReferenceEqualityComparer() { } + + /// + /// Gets the singleton instance. + /// + public static ReferenceEqualityComparer Instance { get; } = new(); + + /// + /// Determines whether two object references refer to the same object instance. + /// + /// The first object to compare. + /// The second object to compare. + /// + /// if both and refer to the same object instance + /// or if both are ; otherwise, . + /// + /// + /// This API is a wrapper around . + /// It is not necessarily equivalent to calling . + /// + public new bool Equals(object x, object y) => ReferenceEquals(x, y); + + /// + /// Returns a hash code for the specified object. The returned hash code is based on the object + /// identity, not on the contents of the object. + /// + /// The object for which to retrieve the hash code. + /// A hash code for the identity of . + /// + /// This API is a wrapper around . + /// It is not necessarily equivalent to calling . + /// + public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj); + } +} diff --git a/Assets/SEE/Utils/ReferenceEqualityComparer.cs.meta b/Assets/SEE/Utils/ReferenceEqualityComparer.cs.meta new file mode 100644 index 0000000000..8240b05faf --- /dev/null +++ b/Assets/SEE/Utils/ReferenceEqualityComparer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ac6b1291562446a8991abbd11d8f0af6 +timeCreated: 1720735805 \ No newline at end of file From 3c99ae881c9a2f27b9af8e961e81186888d070ab Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Wed, 17 Jul 2024 21:54:24 +0200 Subject: [PATCH 03/23] LSP: Implement diagnostic highlighting for code windows #686 --- Assets/SEE/DataModel/DG/IO/LSPImporter.cs | 4 +- Assets/SEE/DataModel/DG/Range.cs | 50 +++ Assets/SEE/Game/City/ErosionAttributes.cs | 12 +- Assets/SEE/Game/City/SEECity.cs | 2 +- .../SEE/Net/Dashboard/DashboardRetriever.cs | 12 +- .../Issues/ArchitectureViolationIssue.cs | 42 +-- .../Net/Dashboard/Model/Issues/CloneIssue.cs | 24 +- .../Net/Dashboard/Model/Issues/CycleIssue.cs | 26 +- .../Dashboard/Model/Issues/DeadEntityIssue.cs | 14 +- .../SEE/Net/Dashboard/Model/Issues/Issue.cs | 54 +++- .../Model/Issues/MetricViolationIssue.cs | 28 +- .../Model/Issues/SourceCodeEntity.cs | 10 +- .../Model/Issues/StyleViolationIssue.cs | 18 +- Assets/SEE/Tools/LSP/LSPHandler.cs | 44 ++- Assets/SEE/Tools/LSP/LSPIssue.cs | 95 ++++++ Assets/SEE/Tools/LSP/LSPIssue.cs.meta | 3 + .../UI/Window/CodeWindow/CodeWindowInput.cs | 295 ++++++++++-------- .../UI/Window/CodeWindow/DesktopCodeWindow.cs | 31 +- .../UI/Window/CodeWindow/DisplayableIssue.cs | 112 +++++++ .../CodeWindow/DisplayableIssue.cs.meta | 3 + Assets/SEE/UI/Window/DesktopWindowSpace.cs | 3 +- Assets/SEE/Utils/CollectionExtensions.cs | 8 +- Assets/SEE/Utils/DefaultDictionary.cs | 2 +- Assets/SEETests/TestConfigIO.cs | 4 +- 24 files changed, 641 insertions(+), 255 deletions(-) create mode 100644 Assets/SEE/Tools/LSP/LSPIssue.cs create mode 100644 Assets/SEE/Tools/LSP/LSPIssue.cs.meta create mode 100644 Assets/SEE/UI/Window/CodeWindow/DisplayableIssue.cs create mode 100644 Assets/SEE/UI/Window/CodeWindow/DisplayableIssue.cs.meta diff --git a/Assets/SEE/DataModel/DG/IO/LSPImporter.cs b/Assets/SEE/DataModel/DG/IO/LSPImporter.cs index 486c4d45c4..80584373d4 100644 --- a/Assets/SEE/DataModel/DG/IO/LSPImporter.cs +++ b/Assets/SEE/DataModel/DG/IO/LSPImporter.cs @@ -325,8 +325,8 @@ public async UniTask LoadAsync(Graph graph, Action changePercentage = nul { // In this case, we will wait one additional second to give the server at least some time to emit diagnostics. // TODO (#746): Collect diagnostics in background, or find a better way to handle this. - await UniTask.Delay(TimeSpan.FromSeconds(1), cancellationToken: token); - foreach (PublishDiagnosticsParams diagnosticsParams in Handler.GetPublishedDiagnostics()) + await UniTask.Delay(Handler.TimeoutSpan, cancellationToken: token); + foreach (PublishDiagnosticsParams diagnosticsParams in Handler.GetUnhandledPublishedDiagnostics()) { HandleDiagnostics(diagnosticsParams.Diagnostics, diagnosticsParams.Uri.Path); } diff --git a/Assets/SEE/DataModel/DG/Range.cs b/Assets/SEE/DataModel/DG/Range.cs index 2467dc6698..8b223c3bf7 100644 --- a/Assets/SEE/DataModel/DG/Range.cs +++ b/Assets/SEE/DataModel/DG/Range.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using UnityEngine.Assertions; namespace SEE.DataModel.DG @@ -35,6 +36,45 @@ public record Range(int StartLine, int EndLine, int? StartCharacter = null, int? /// public (int Line, int Character) End => (EndLine, EndCharacter ?? 0); + /// + /// Whether this range has a character range. + /// If this is false, the range only contains full lines. + /// + public bool HasCharacter => StartCharacter.HasValue || EndCharacter.HasValue; + + /// + /// Splits this range into individual lines. + /// The resulting ranges, taken together, will cover the same area as this range, + /// but each range will only cover a single line at most. + /// + /// An enumerable of ranges, each covering a single line. + public IEnumerable SplitIntoLines() + { + if (Lines <= 1) + { + // Just the one line. + yield return this; + } + else if (HasCharacter) + { + // If we have characters, we need to split the first and last line. + yield return this with { EndLine = StartLine + 1, EndCharacter = null }; + for (int line = StartLine + 1; line < EndLine; line++) + { + yield return new Range(line, line + 1); + } + yield return this with { StartLine = EndLine - 1, StartCharacter = 0 }; + } + else + { + // If we don't have characters, we can just return the full lines. + for (int line = StartLine; line < EndLine; line++) + { + yield return new Range(line, line + 1); + } + } + } + /// /// Returns true if the given line and character are contained in this range. /// @@ -91,6 +131,16 @@ public bool Contains(Range other) return contains; } + /// + /// Whether this range overlaps with the given line. + /// + /// The line to check. + /// True if this range overlaps with the given line. + public bool Overlaps(int line) + { + return line >= StartLine && line < EndLine; + } + public override string ToString() { return StartCharacter.HasValue && EndCharacter.HasValue diff --git a/Assets/SEE/Game/City/ErosionAttributes.cs b/Assets/SEE/Game/City/ErosionAttributes.cs index 90f74120a9..cc55803875 100644 --- a/Assets/SEE/Game/City/ErosionAttributes.cs +++ b/Assets/SEE/Game/City/ErosionAttributes.cs @@ -3,6 +3,7 @@ using SEE.DataModel.DG; using SEE.Utils.Config; using UnityEngine; +using UnityEngine.Serialization; namespace SEE.Game.City { @@ -34,9 +35,10 @@ public class ErosionAttributes : VisualAttributes public float ErosionScalingFactor = 1.5f; /// - /// Whether code issues should be downloaded and shown in code viewers. + /// Whether code issues from the Axivion Dashboard should be downloaded and shown in code viewers. /// - public bool ShowIssuesInCodeWindow = false; + [FormerlySerializedAs("ShowIssuesInCodeWindow")] + public bool ShowDashboardIssuesInCodeWindow = false; /// /// The attribute name of the metric representing architecture violations. @@ -129,7 +131,7 @@ public override void Save(ConfigWriter writer, string label) writer.BeginGroup(label); writer.Save(ShowInnerErosions, showInnerErosionsLabel); writer.Save(ShowLeafErosions, showLeafErosionsLabel); - writer.Save(ShowIssuesInCodeWindow, showIssuesInCodeWindowLabel); + writer.Save(ShowDashboardIssuesInCodeWindow, showIssuesInCodeWindowLabel); writer.Save(ErosionScalingFactor, erosionScalingFactorLabel); writer.Save(StyleIssue, styleIssueLabel); @@ -162,7 +164,7 @@ public override void Restore(Dictionary attributes, string label ConfigIO.Restore(values, showInnerErosionsLabel, ref ShowInnerErosions); ConfigIO.Restore(values, showLeafErosionsLabel, ref ShowLeafErosions); - ConfigIO.Restore(values, showIssuesInCodeWindowLabel, ref ShowIssuesInCodeWindow); + ConfigIO.Restore(values, showIssuesInCodeWindowLabel, ref ShowDashboardIssuesInCodeWindow); ConfigIO.Restore(values, erosionScalingFactorLabel, ref ErosionScalingFactor); ConfigIO.Restore(values, styleIssueLabel, ref StyleIssue); @@ -190,7 +192,7 @@ public override void Restore(Dictionary attributes, string label private const string showLeafErosionsLabel = "ShowLeafErosions"; private const string showInnerErosionsLabel = "ShowInnerErosions"; private const string erosionScalingFactorLabel = "ErosionScalingFactor"; - private const string showIssuesInCodeWindowLabel = "ShowIssuesInCodeWindow"; + private const string showIssuesInCodeWindowLabel = "ShowDashboardIssuesInCodeWindow"; private const string styleIssueLabel = "StyleIssue"; private const string universalIssueLabel = "UniversalIssue"; diff --git a/Assets/SEE/Game/City/SEECity.cs b/Assets/SEE/Game/City/SEECity.cs index c9bd455a0a..54ed2144fa 100644 --- a/Assets/SEE/Game/City/SEECity.cs +++ b/Assets/SEE/Game/City/SEECity.cs @@ -305,7 +305,7 @@ public virtual async UniTask LoadDataAsync() { try { - using (LoadingSpinner.ShowDeterminate($"Loading city \"{gameObject.name}\"...\n", + using (LoadingSpinner.ShowDeterminate($"Loading city \"{gameObject.name}\"...", out Action reportProgress)) { void ReportProgress(float x) diff --git a/Assets/SEE/Net/Dashboard/DashboardRetriever.cs b/Assets/SEE/Net/Dashboard/DashboardRetriever.cs index 433a189a3d..0d4bb8d943 100644 --- a/Assets/SEE/Net/Dashboard/DashboardRetriever.cs +++ b/Assets/SEE/Net/Dashboard/DashboardRetriever.cs @@ -310,12 +310,12 @@ private async UniTaskVoid VerifyVersionNumberAsync() public Color GetIssueColor(Issue issue) => issue switch { - ArchitectureViolationIssue _ => ArchitectureViolationIssueColor, - CloneIssue _ => CloneIssueColor, - CycleIssue _ => CycleIssueColor, - DeadEntityIssue _ => DeadEntityIssueColor, - MetricViolationIssue _ => MetricViolationIssueColor, - StyleViolationIssue _ => StyleViolationIssueColor, + ArchitectureViolationIssue => ArchitectureViolationIssueColor, + CloneIssue => CloneIssueColor, + CycleIssue => CycleIssueColor, + DeadEntityIssue => DeadEntityIssueColor, + MetricViolationIssue => MetricViolationIssueColor, + StyleViolationIssue => StyleViolationIssueColor, _ => throw new ArgumentOutOfRangeException(nameof(issue), issue, "Unknown issue kind!") }; diff --git a/Assets/SEE/Net/Dashboard/Model/Issues/ArchitectureViolationIssue.cs b/Assets/SEE/Net/Dashboard/Model/Issues/ArchitectureViolationIssue.cs index be94c1d549..b7c41ef28b 100644 --- a/Assets/SEE/Net/Dashboard/Model/Issues/ArchitectureViolationIssue.cs +++ b/Assets/SEE/Net/Dashboard/Model/Issues/ArchitectureViolationIssue.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Linq; using Cysharp.Threading.Tasks; +using Newtonsoft.Json; using SEE.DataModel.DG; using SEE.Utils; -using Newtonsoft.Json; namespace SEE.Net.Dashboard.Model.Issues { @@ -143,25 +143,25 @@ protected ArchitectureViolationIssue(string architectureSource, string architect string targetEntityType, string targetPath, int targetLine, string targetLinkName) { - this.ArchitectureSource = architectureSource; - this.ArchitectureSourceType = architectureSourceType; - this.ArchitectureSourceLinkName = architectureSourceLinkName; - this.ArchitectureTarget = architectureTarget; - this.ArchitectureTargetType = architectureTargetType; - this.ArchitectureTargetLinkName = architectureTargetLinkName; - this.ErrorNumber = errorNumber; - this.ViolationType = violationType; - this.DependencyType = dependencyType; - this.SourceEntity = sourceEntity; - this.SourceEntityType = sourceEntityType; - this.SourcePath = sourcePath; - this.SourceLine = sourceLine; - this.SourceLinkName = sourceLinkName; - this.TargetEntity = targetEntity; - this.TargetEntityType = targetEntityType; - this.TargetPath = targetPath; - this.TargetLine = targetLine; - this.TargetLinkName = targetLinkName; + ArchitectureSource = architectureSource; + ArchitectureSourceType = architectureSourceType; + ArchitectureSourceLinkName = architectureSourceLinkName; + ArchitectureTarget = architectureTarget; + ArchitectureTargetType = architectureTargetType; + ArchitectureTargetLinkName = architectureTargetLinkName; + ErrorNumber = errorNumber; + ViolationType = violationType; + DependencyType = dependencyType; + SourceEntity = sourceEntity; + SourceEntityType = sourceEntityType; + SourcePath = sourcePath; + SourceLine = sourceLine; + SourceLinkName = sourceLinkName; + TargetEntity = targetEntity; + TargetEntityType = targetEntityType; + TargetPath = targetPath; + TargetLine = targetLine; + TargetLinkName = targetLinkName; } public override async UniTask ToDisplayStringAsync() @@ -187,4 +187,4 @@ public override async UniTask ToDisplayStringAsync() }.Where(x => x.path != null) .Select(x => new SourceCodeEntity(x.path, x.line, null, x.entity)); } -} \ No newline at end of file +} diff --git a/Assets/SEE/Net/Dashboard/Model/Issues/CloneIssue.cs b/Assets/SEE/Net/Dashboard/Model/Issues/CloneIssue.cs index b7db44f2bc..c412b10f59 100644 --- a/Assets/SEE/Net/Dashboard/Model/Issues/CloneIssue.cs +++ b/Assets/SEE/Net/Dashboard/Model/Issues/CloneIssue.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using Cysharp.Threading.Tasks; +using Newtonsoft.Json; using SEE.DataModel.DG; using SEE.Utils; -using Newtonsoft.Json; namespace SEE.Net.Dashboard.Model.Issues { @@ -89,17 +89,17 @@ public CloneIssue(int cloneType, string leftPath, int leftLine, int leftEndLine, int leftWeight, string rightPath, int rightLine, int rightEndLine, int rightLength, int rightWeight) { - this.CloneType = cloneType; - this.LeftPath = leftPath; - this.LeftLine = leftLine; - this.LeftEndLine = leftEndLine; - this.LeftLength = leftLength; - this.LeftWeight = leftWeight; - this.RightPath = rightPath; - this.RightLine = rightLine; - this.RightEndLine = rightEndLine; - this.RightLength = rightLength; - this.RightWeight = rightWeight; + CloneType = cloneType; + LeftPath = leftPath; + LeftLine = leftLine; + LeftEndLine = leftEndLine; + LeftLength = leftLength; + LeftWeight = leftWeight; + RightPath = rightPath; + RightLine = rightLine; + RightEndLine = rightEndLine; + RightLength = rightLength; + RightWeight = rightWeight; } public override async UniTask ToDisplayStringAsync() diff --git a/Assets/SEE/Net/Dashboard/Model/Issues/CycleIssue.cs b/Assets/SEE/Net/Dashboard/Model/Issues/CycleIssue.cs index 0517f977ad..9afcdf137a 100644 --- a/Assets/SEE/Net/Dashboard/Model/Issues/CycleIssue.cs +++ b/Assets/SEE/Net/Dashboard/Model/Issues/CycleIssue.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using Cysharp.Threading.Tasks; +using Newtonsoft.Json; using SEE.DataModel.DG; using SEE.Utils; -using Newtonsoft.Json; namespace SEE.Net.Dashboard.Model.Issues { @@ -89,17 +89,17 @@ protected CycleIssue(string dependencyType, string sourceEntity, string sourceEn string sourcePath, int sourceLine, string sourceLinkName, string targetEntity, string targetEntityType, string targetPath, int targetLine, string targetLinkName) { - this.DependencyType = dependencyType; - this.SourceEntity = sourceEntity; - this.SourceEntityType = sourceEntityType; - this.SourcePath = sourcePath; - this.SourceLine = sourceLine; - this.SourceLinkName = sourceLinkName; - this.TargetEntity = targetEntity; - this.TargetEntityType = targetEntityType; - this.TargetPath = targetPath; - this.TargetLine = targetLine; - this.TargetLinkName = targetLinkName; + DependencyType = dependencyType; + SourceEntity = sourceEntity; + SourceEntityType = sourceEntityType; + SourcePath = sourcePath; + SourceLine = sourceLine; + SourceLinkName = sourceLinkName; + TargetEntity = targetEntity; + TargetEntityType = targetEntityType; + TargetPath = targetPath; + TargetLine = targetLine; + TargetLinkName = targetLinkName; } public override async UniTask ToDisplayStringAsync() @@ -121,4 +121,4 @@ public override async UniTask ToDisplayStringAsync() new SourceCodeEntity(TargetPath, TargetLine, null, TargetEntity) }; } -} \ No newline at end of file +} diff --git a/Assets/SEE/Net/Dashboard/Model/Issues/DeadEntityIssue.cs b/Assets/SEE/Net/Dashboard/Model/Issues/DeadEntityIssue.cs index 89e444be7e..d01d96d303 100644 --- a/Assets/SEE/Net/Dashboard/Model/Issues/DeadEntityIssue.cs +++ b/Assets/SEE/Net/Dashboard/Model/Issues/DeadEntityIssue.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using Cysharp.Threading.Tasks; +using Newtonsoft.Json; using SEE.DataModel.DG; using SEE.Utils; -using Newtonsoft.Json; namespace SEE.Net.Dashboard.Model.Issues { @@ -51,11 +51,11 @@ public DeadEntityIssue() [JsonConstructor] protected DeadEntityIssue(string entity, string entityType, string path, int line, string linkName) { - this.Entity = entity; - this.EntityType = entityType; - this.Path = path; - this.Line = line; - this.LinkName = linkName; + Entity = entity; + EntityType = entityType; + Path = path; + Line = line; + LinkName = linkName; } public override async UniTask ToDisplayStringAsync() @@ -75,4 +75,4 @@ public override async UniTask ToDisplayStringAsync() new SourceCodeEntity(Path, Line, null, Entity) }; } -} \ No newline at end of file +} diff --git a/Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs b/Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs index 798f230554..a8f93690b0 100644 --- a/Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs +++ b/Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs @@ -1,9 +1,14 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; using Cysharp.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using SEE.DataModel.DG; +using SEE.UI.Window.CodeWindow; +using UnityEngine; +using Range = SEE.DataModel.DG.Range; namespace SEE.Net.Dashboard.Model.Issues { @@ -11,7 +16,7 @@ namespace SEE.Net.Dashboard.Model.Issues /// Contains information about an issue in the source code. /// [Serializable] - public abstract class Issue + public abstract class Issue : IDisplayableIssue { // A note: Due to how the JSON serializer works with inheritance, fields in here can't be readonly. @@ -24,15 +29,15 @@ public enum IssueState } /// - /// A kind-wide Id identifying the issue across analysis versions + /// A kind-wide ID identifying the issue across analysis versions /// [JsonProperty(PropertyName = "id", Required = Required.Always)] public int ID; /// /// In diff-queries, this indicates whether the issue is “Removed”, - /// i.e. contained in the base-version but not any more in the current version or “Added”, - /// i.e. it was not contained in the base-version but is contained in the current version + /// i.e. contained in the base-version but not anymore in the current version or “Added”, + /// that is, it was not contained in the base-version but is contained in the current version. /// [JsonConverter(typeof(StringEnumConverter))] [JsonProperty(PropertyName = "state", Required = Required.Default)] @@ -71,7 +76,7 @@ public enum IssueState /// /// The dashboard users associated with the issue via VCS blaming, CI path mapping, - /// CI user name mapping and dashboard user name mapping. + /// CI username mapping and dashboard username mapping. /// [JsonProperty(PropertyName = "owners", Required = Required.Always)] public IList Owners; @@ -81,8 +86,8 @@ protected Issue() // Necessary for inheritance with Newtonsoft.Json to work properly } - public Issue(int id, IssueState state, bool suppressed, string justification, - IList tag, IList comments, IList owners) + protected Issue(int id, IssueState state, bool suppressed, string justification, + IList tag, IList comments, IList owners) { ID = id; State = state; @@ -126,7 +131,7 @@ public IssueTag(string tag, string color) public readonly struct IssueComment { /// - /// The loginname of the user that created the comment. + /// The login name of the user that created the comment. /// [JsonProperty(PropertyName = "username", Required = Required.Always)] public readonly string Username; @@ -207,5 +212,38 @@ public IssueComment(string username, string userDisplayName, DateTime date, /// May be empty if all referenced entities don't have a path. /// public abstract IEnumerable Entities { get; } + + /// + /// The color to use when marking this issue in code windows. + /// + public Color Color => DashboardRetriever.Instance.GetIssueColor(this); + + public IList RichTags => new List + { + // Use a transparency value of 0x33 + $"" + }; + + public string Source => "Axivion Dashboard"; + + public IEnumerable<(string Path, Range Range)> Occurrences => Entities.Select(e => (e.Path, new Range(e.Line, (e.EndLine ?? e.Line) + 1))); + + public (int startCharacter, int endCharacter)? GetCharacterRangeForLine(string path, int lineNumber, string line) + { + // Axivion Dashboard doesn't provide character ranges for issues, so we have to calculate them ourselves. + SourceCodeEntity entity = Entities.FirstOrDefault(e => e.Path == path && e.Line == lineNumber); + if (entity != null) + { + MatchCollection matches = Regex.Matches(line, Regex.Escape(entity.Content)); + // We return null if we found more than one occurence too, because in that case + // we have no way to determine which of the occurrences is the right one. + if (matches.Count == 1) + { + Match match = matches[0]; + return (match.Index, match.Index + match.Length); + } + } + return null; + } } } diff --git a/Assets/SEE/Net/Dashboard/Model/Issues/MetricViolationIssue.cs b/Assets/SEE/Net/Dashboard/Model/Issues/MetricViolationIssue.cs index 04d5a29f68..cc4ffdd19f 100644 --- a/Assets/SEE/Net/Dashboard/Model/Issues/MetricViolationIssue.cs +++ b/Assets/SEE/Net/Dashboard/Model/Issues/MetricViolationIssue.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using Cysharp.Threading.Tasks; +using Newtonsoft.Json; using SEE.DataModel.DG; using SEE.Utils; -using Newtonsoft.Json; namespace SEE.Net.Dashboard.Model.Issues { @@ -101,18 +101,18 @@ public MetricViolationIssue(string severity, string entity, string entityType, s string linkName, string metric, string errorNumber, string description, float? max, float? min, float value) { - this.Severity = severity; - this.Entity = entity; - this.EntityType = entityType; - this.Path = path; - this.Line = line; - this.LinkName = linkName; - this.Metric = metric; - this.ErrorNumber = errorNumber; - this.Description = description; - this.Max = max; - this.Min = min; - this.Value = value; + Severity = severity; + Entity = entity; + EntityType = entityType; + Path = path; + Line = line; + LinkName = linkName; + Metric = metric; + ErrorNumber = errorNumber; + Description = description; + Max = max; + Min = min; + Value = value; } public override async UniTask ToDisplayStringAsync() @@ -137,4 +137,4 @@ public override async UniTask ToDisplayStringAsync() new SourceCodeEntity(Path, Line, null, Entity) }; } -} \ No newline at end of file +} diff --git a/Assets/SEE/Net/Dashboard/Model/Issues/SourceCodeEntity.cs b/Assets/SEE/Net/Dashboard/Model/Issues/SourceCodeEntity.cs index ceb5a21857..e0aaabc18c 100644 --- a/Assets/SEE/Net/Dashboard/Model/Issues/SourceCodeEntity.cs +++ b/Assets/SEE/Net/Dashboard/Model/Issues/SourceCodeEntity.cs @@ -35,10 +35,10 @@ public class SourceCodeEntity public SourceCodeEntity(string path, int line, int? endLine = null, string content = null) { - this.Path = path ?? throw new ArgumentNullException(nameof(path)); - this.Line = line; - this.EndLine = endLine; - this.Content = string.IsNullOrEmpty(content) ? null : content; + Path = path ?? throw new ArgumentNullException(nameof(path)); + Line = line; + EndLine = endLine; + Content = string.IsNullOrEmpty(content) ? null : content; } } -} \ No newline at end of file +} diff --git a/Assets/SEE/Net/Dashboard/Model/Issues/StyleViolationIssue.cs b/Assets/SEE/Net/Dashboard/Model/Issues/StyleViolationIssue.cs index 70ada5b824..05d24ed39d 100644 --- a/Assets/SEE/Net/Dashboard/Model/Issues/StyleViolationIssue.cs +++ b/Assets/SEE/Net/Dashboard/Model/Issues/StyleViolationIssue.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using Cysharp.Threading.Tasks; +using Newtonsoft.Json; using SEE.DataModel.DG; using SEE.Utils; -using Newtonsoft.Json; namespace SEE.Net.Dashboard.Model.Issues { @@ -64,13 +64,13 @@ public StyleViolationIssue() public StyleViolationIssue(string severity, string provider, string errorNumber, string message, string entity, string path, int line) { - this.Severity = severity; - this.Provider = provider; - this.ErrorNumber = errorNumber; - this.Message = message; - this.Entity = entity; - this.Path = path; - this.Line = line; + Severity = severity; + Provider = provider; + ErrorNumber = errorNumber; + Message = message; + Entity = entity; + Path = path; + Line = line; } public override async UniTask ToDisplayStringAsync() @@ -88,4 +88,4 @@ public override async UniTask ToDisplayStringAsync() new SourceCodeEntity(Path, Line, null, Entity) }; } -} \ No newline at end of file +} diff --git a/Assets/SEE/Tools/LSP/LSPHandler.cs b/Assets/SEE/Tools/LSP/LSPHandler.cs index 2155182cc2..fc84d552d4 100644 --- a/Assets/SEE/Tools/LSP/LSPHandler.cs +++ b/Assets/SEE/Tools/LSP/LSPHandler.cs @@ -119,6 +119,11 @@ public LSPServer Server /// private readonly ConcurrentQueue unhandledDiagnostics = new(); + /// + /// A dictionary mapping from file paths to the diagnostics that have been published for that file. + /// + private readonly Dictionary> savedDiagnostics = new(); + /// /// The capabilities of the language client. /// @@ -165,8 +170,19 @@ public LSPServer Server }, LabelSupport = false }, - Diagnostic = new DiagnosticClientCapabilities(), - PublishDiagnostics = new PublishDiagnosticsCapability(), + Diagnostic = new DiagnosticClientCapabilities + { + RelatedDocumentSupport = true + }, + PublishDiagnostics = new PublishDiagnosticsCapability + { + RelatedInformation = true, + VersionSupport = false, + TagSupport = new Supports(new PublishDiagnosticsTagSupportCapabilityOptions + { + ValueSet = Container.From(DiagnosticTag.Unnecessary, DiagnosticTag.Deprecated) + }) + }, SemanticTokens = new SemanticTokensCapability() { Requests = new SemanticTokensCapabilityRequests() @@ -258,6 +274,8 @@ public async UniTask InitializeAsync(string executablePath = null, CancellationT return; } + savedDiagnostics.Clear(); + unhandledDiagnostics.Clear(); HashSet initialWork = new(); IDisposable spinner = LoadingSpinner.ShowIndeterminate("Starting language server..."); try @@ -378,12 +396,14 @@ async UniTaskVoid MonitorInitialWorkDoneProgress(ProgressToken progressToken) /// /// Handles the diagnostics published by the language server by storing them - /// in the queue. + /// in the queue, as well as + /// in the dictionary. /// /// The parameters of the diagnostics. private void HandleDiagnostics(PublishDiagnosticsParams diagnosticsParams) { unhandledDiagnostics.Enqueue(diagnosticsParams); + savedDiagnostics.GetOrAdd(diagnosticsParams.Uri.GetFileSystemPath(), () => new()).Add(diagnosticsParams); } /// @@ -510,7 +530,7 @@ public async UniTask HoverAsync(string path, int line, int character = 0) /// compared to the last call, the method returns null. /// /// Note that this is a very new feature (LSP 3.17) and not all language servers support it. - /// An alternative is to use the method to + /// An alternative is to use the method to /// retrieve the diagnostics that have been published by the language server. /// /// The path to the document. @@ -530,8 +550,8 @@ public async UniTask> PullDocumentDiagnosticsAsync(strin /// /// Retrieves the unhandled diagnostics that have been published by the language server. /// - /// An enumerable of the published diagnostics. - public IEnumerable GetPublishedDiagnostics() + /// An enumerable of the unhandled published diagnostics. + public IEnumerable GetUnhandledPublishedDiagnostics() { while (unhandledDiagnostics.TryDequeue(out PublishDiagnosticsParams diagnostics)) { @@ -539,6 +559,18 @@ public IEnumerable GetPublishedDiagnostics() } } + /// + /// Returns the diagnostics that were saved for the given . + /// Note that this may not include every diagnostic the language server would have sent, + /// as we only listen to published diagnostics for a certain timeframe (see ). + /// + /// The path for which to retrieve the diagnostics. + /// The published diagnostics for the given path. + public IEnumerable GetPublishedDiagnosticsForPath(string path) + { + return savedDiagnostics.GetValueOrDefault(path) ?? Enumerable.Empty(); + } + /// /// Retrieves all references to the symbol in the document with the given at the given /// and . diff --git a/Assets/SEE/Tools/LSP/LSPIssue.cs b/Assets/SEE/Tools/LSP/LSPIssue.cs new file mode 100644 index 0000000000..73e9bcd2d1 --- /dev/null +++ b/Assets/SEE/Tools/LSP/LSPIssue.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Cysharp.Threading.Tasks; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using SEE.UI.Window.CodeWindow; +using Range = SEE.DataModel.DG.Range; + +namespace SEE.Tools.LSP +{ + /// + /// A code issue diagnosed by a language server. + /// + /// The path of the file where the issue was diagnosed. + /// The diagnostic that represents the issue. + public record LSPIssue(string Path, Diagnostic Diagnostic) : IDisplayableIssue + { + public UniTask ToDisplayStringAsync() + { + string message = ""; + if (Diagnostic.Code.HasValue) + { + message += $"{Diagnostic.Code.Value.String ?? Diagnostic.Code.Value.Long.ToString()}: "; + } + message += $"{Diagnostic.Message}"; + return UniTask.FromResult(message); + } + + public string Source => Diagnostic.Source ?? "LSP"; + + public IList RichTags + { + get + { + List tags = Diagnostic.Tags?.ToList() ?? new(); + if (tags.Count > 0) + { + return tags.Select(DiagnosticTagToRichTag).ToList(); + } + else + { + // If there are no explicit tags, we create a tag based on the severity. + return new List + { + DiagnosticSeverityToTag(Diagnostic.Severity ?? DiagnosticSeverity.Warning) + }; + } + } + } + + /// + /// Converts a diagnostic tag to a TextMeshPro rich text tag, intended to be used within code windows. + /// + /// The diagnostic tag to convert. + /// The TextMeshPro rich text tag that corresponds to the given . + private static string DiagnosticTagToRichTag(DiagnosticTag tag) => + tag switch + { + DiagnosticTag.Unnecessary => "", + DiagnosticTag.Deprecated => "", + _ => throw new ArgumentOutOfRangeException(nameof(tag), tag, "Unknown diagnostic tag") + }; + + /// + /// Converts a diagnostic severity to a TextMeshPro rich text tag, intended to be used within code windows. + /// + /// The diagnostic severity to convert. + /// The TextMeshPro rich text tag that corresponds to the given . + private static string DiagnosticSeverityToTag(DiagnosticSeverity severity) => + severity switch + { + DiagnosticSeverity.Error => "", + DiagnosticSeverity.Warning => "", + DiagnosticSeverity.Information => "", + DiagnosticSeverity.Hint => "", + _ => throw new ArgumentOutOfRangeException(nameof(severity), severity, "Unknown diagnostic severity") + }; + + public IEnumerable<(string Path, Range Range)> Occurrences + { + get + { + List<(string Path, Range Range)> occurrences = new() + { + (Path, Range.FromLspRange(Diagnostic.Range)) + }; + if (Diagnostic.RelatedInformation != null) + { + occurrences.AddRange(Diagnostic.RelatedInformation.Select(x => (x.Location.Uri.GetFileSystemPath(), Range.FromLspRange(x.Location.Range)))); + } + return occurrences; + } + } + } +} diff --git a/Assets/SEE/Tools/LSP/LSPIssue.cs.meta b/Assets/SEE/Tools/LSP/LSPIssue.cs.meta new file mode 100644 index 0000000000..5f566e2bb2 --- /dev/null +++ b/Assets/SEE/Tools/LSP/LSPIssue.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0f8ef404e0e848dbacec41015bca6a4e +timeCreated: 1720786567 \ No newline at end of file diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs b/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs index bd278cac61..957bc749db 100644 --- a/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using Cysharp.Threading.Tasks; using SEE.Game; using SEE.Game.City; @@ -33,7 +32,7 @@ public partial class CodeWindow /// /// A dictionary mapping each link ID to its issues. /// - private readonly Dictionary> issueDictionary = new(); + private readonly Dictionary> issueDictionary = new(); /// /// Counter which represents the lowest unfilled position in the . @@ -61,8 +60,7 @@ public partial class CodeWindow /// If you wish to use such issues, split the entities up into one per line (see ). /// /// If is null. - public void EnterFromTokens(IEnumerable tokens, - IDictionary> issues = null) + private void EnterFromTokens(IEnumerable tokens, IDictionary> issues = null) { if (tokens == null) { @@ -87,8 +85,10 @@ public void EnterFromTokens(IEnumerable tokens, text = $"{string.Join("", Enumerable.Repeat(" ", neededPadding - 1))}1 "; int lineNumber = 2; // Line number we'll write down next - bool currentlyMarking = false; - Dictionary> issueTokens = new(); + // The issue that we're currently marking, if any. + IDisplayableIssue currentlyMarking = null; + // We need reference equality here. + Dictionary> issueTokens = new(ReferenceEqualityComparer.Instance); foreach (SEEToken token in tokenList) { @@ -102,6 +102,9 @@ public void EnterFromTokens(IEnumerable tokens, } } + // End any issue marking that may still be open. + EndIssueSegment(); + // Lines are equal to number of newlines, including the initial newline. lines = text.Count(x => x.Equals('\n')); // No more weird CRLF shenanigans are present at this point. text = text.TrimStart('\n'); // Remove leading newline. @@ -113,29 +116,18 @@ public void EnterFromTokens(IEnumerable tokens, void AppendNewline(ref int theLineNumber, ref string text, int padding, SEEToken token) { // Close an issue marking here if necessary - if (currentlyMarking) - { - text += ""; - currentlyMarking = false; - } + EndIssueSegment(); // First, of course, the newline. text += "\n"; - // Add whitespace next to line number so it's consistent. + // Add whitespace next to line number, so it's consistent. text += string.Join("", Enumerable.Repeat(" ", padding - $"{theLineNumber}".Length)); // Line number will be typeset in grey to distinguish it from the rest. text += $"{theLineNumber} "; if (issues?.ContainsKey(theLineNumber) ?? false) { - // If all issues in this line are content-based, we try to find the content within the line - if (issues[theLineNumber].Exists(x => x.entity.Content == null) - || !HandleContentBasedIssue(theLineNumber, token)) - { - // Otherwise, start new issue marking here if an issue in the line is line-based (has no content) - // or if we couldn't do the content-based issue marking for any reason. - HandleLineBasedIssue(theLineNumber, ref text); - } + HandleIssuesInLine(theLineNumber, token); } theLineNumber++; @@ -164,21 +156,33 @@ int HandleToken(SEEToken token) // Mark any potential issue if (issueTokens.ContainsKey(token) && issueTokens[token].Count > 0) { - if (currentlyMarking) + if (currentlyMarking != null) { - // If this line is already fully marked, we just add our issue to the corresponding link + // We're already marking something. Assert.IsNotNull(issueDictionary[linkCounter], "Entry must exist when we are currently marking!"); - issueDictionary[linkCounter].AddRange(issueTokens[token]); + if (issueTokens[token].Intersect(issueDictionary[linkCounter]).Any()) + { + // If this token contains the same issue, we just need to add any new issues to the current segment. + issueDictionary[linkCounter].UnionWith(issueTokens[token]); + } + else + { + // If it doesn't, we close the current segment and start a new one. + EndIssueSegment(); + StartIssueSegment(token); + } } else { - Color issueColor = DashboardRetriever.Instance.GetIssueColor(issueTokens[token].First()); - string issueColorString = ColorUtility.ToHtmlStringRGB(issueColor); - IncreaseLinkCounter(); - issueDictionary[linkCounter] = issueTokens[token].ToList(); - text += $""; + // We're not marking anything, so we can start a new segment. + StartIssueSegment(token); } } + else + { + // No issue is being marked. We should stop marking if we were marking something. + EndIssueSegment(); + } if (token.TokenType == TokenType.Whitespace) { @@ -194,9 +198,15 @@ int HandleToken(SEEToken token) { text += $"<{textTag}>"; } - text += $""; + if (currentlyMarking is not { HasColorTags: true }) + { + text += $""; + } text += $"{line.Replace("/noparse", "")}"; - text += ""; + if (currentlyMarking is not { HasColorTags: true }) + { + text += ""; + } tags.Reverse(); foreach (string textTag in tags) { @@ -204,42 +214,57 @@ int HandleToken(SEEToken token) } } - // Close any potential issue marking - if (issueTokens.ContainsKey(token) && !currentlyMarking) - { - text += ""; - } - firstRun = false; } return lineNumber; } - // Returns true iff the content based issue could correctly be inserted - bool HandleContentBasedIssue(int theLineNumber, SEEToken currentToken) + // Begins marking a new issue segment starting with the given token. + void StartIssueSegment(SEEToken token) + { + Assert.IsNull(currentlyMarking, "We must not start a new marking segment while we're already marking!"); + IncreaseLinkCounter(); + issueDictionary[linkCounter] = issueTokens[token].ToHashSet(); + currentlyMarking = issueTokens[token].First(); + text += $"{currentlyMarking.OpeningRichTags}"; + } + + // Ends marking the current issue segment, if there is any. + void EndIssueSegment() + { + if (currentlyMarking != null) + { + text += $"{currentlyMarking.ClosingRichTags}"; + } + currentlyMarking = null; + } + + // Prepares issueTokens for the line number, assuming the current token is the newline token + // delineating the beginning of this line. + void HandleIssuesInLine(int theLineNumber, SEEToken currentToken) { - // Note: If there are any performance issues, I suspect the following loop body to be a major - // contender for optimization. The easiest fix at the loss of functionality would be - // to simply not mark the issues by content, but instead only use line-based markings. - foreach ((SourceCodeEntity entity, Issue issue) in issues[theLineNumber]) + // We have to determine whether a given token is part of an issue entity. + // In order to do this, we look ahead in the token stream and construct the line we're on + // to determine whether the entity will arrive in this line or not. + IList lineTokens = + tokenList.SkipWhile(x => !ReferenceEquals(x, currentToken)).Skip(1) + .TakeWhile(x => x.TokenType != TokenType.Newline + && !x.Text.Intersect(newlineCharacters).Any()).ToList(); + string line = lineTokens.Aggregate(string.Empty, (s, t) => s + t.Text); + + foreach (IDisplayableIssue issue in issues[theLineNumber]) { - string entityContent = entity.Content; - // We now have to determine whether this token is part of an issue entity. - // In order to do this, we look ahead in the token stream and construct the line we're on - // to determine whether the entity will arrive in this line or not. - IList lineTokens = - tokenList.SkipWhile(x => x != currentToken).Skip(1) - .TakeWhile(x => x.TokenType != TokenType.Newline - && !x.Text.Intersect(newlineCharacters).Any()).ToList(); - string line = lineTokens.Aggregate("", (s, t) => s + t.Text); - MatchCollection matches = Regex.Matches(line, Regex.Escape(entityContent)); - if (matches.Count != 1) + (int startCharacter, int endCharacter)? characterRange = issue.GetCharacterRangeForLine(FilePath, theLineNumber, line); + if (!characterRange.HasValue) { // Switch to line-based marking instead. - // We do this if we found more than one occurence too, because in that case - // we have no way to determine which of the occurrences is the right one. - return false; + IEnumerable matchTokens = lineTokens.SkipWhile(t => t.TokenType == TokenType.Whitespace); + foreach (SEEToken matchToken in matchTokens) + { + issueTokens.GetOrAdd(matchToken, () => new HashSet()).UnionWith(issues[theLineNumber]); + } + return; } else { @@ -247,44 +272,23 @@ bool HandleContentBasedIssue(int theLineNumber, SEEToken currentToken) // Note that this implies that we assume an entity will always encompass only whole // tokens, never just parts of tokens. If this doesn't hold, the whole token will be // highlighted anyway. - // TODO: It is possible to implement an algorithm which can also handle that, - // but that won't be done here, since it's out of scope. // We first create a list of character-wise parts of the tokens, then match - // using the regex's index and length. - IList matchTokens = lineTokens.SelectMany(t => t.Text.Select(_ => t)) - .Skip(matches[0].Index) - .Take(entityContent.Length).ToList(); - foreach (SEEToken matchToken in matchTokens.ToList()) + // using the result's index and length. + IEnumerable matchTokens = lineTokens + .SelectMany(t => Enumerable.Repeat(t, t.Text.Length)) + .Skip(characterRange.Value.startCharacter) + // Exclusive end character. + .Take(characterRange.Value.endCharacter - characterRange.Value.startCharacter - 1); + foreach (SEEToken matchToken in matchTokens) { - if (!issueTokens.ContainsKey(matchToken)) - { - issueTokens[matchToken] = new HashSet(); - } - - issueTokens[matchToken].Add(issue); + issueTokens.GetOrAdd(matchToken, () => new HashSet()).Add(issue); } - - Assert.IsTrue(matchTokens.Count > 0); // Regex Match necessitates at least 1 occurence! } } - - return true; } - // Returns the last line number which is part of the issue starting in this line - void HandleLineBasedIssue(int theLineNumber, ref string text) - { - // Limitation: We can only use the color of the first issue, because we can't reliably detect the - // order of the entities within a single line. Details for all issues are shown on hover. - string issueColor = ColorUtility.ToHtmlStringRGB(DashboardRetriever.Instance.GetIssueColor(issues[theLineNumber][0].issue)); - IncreaseLinkCounter(); - issueDictionary[linkCounter] = issues[theLineNumber].Select(x => x.issue).ToList(); - text += $""; //Transparency value of 0x33 - currentlyMarking = true; - } - - // Increases the link counter to its next value + // Increases the link counter to its next value. void IncreaseLinkCounter() { Assert.IsTrue(linkCounter < char.MaxValue); @@ -395,9 +399,11 @@ public async UniTask EnterFromFileAsync(string filename) SetupBreakpoints(); } - if (go.TryGetComponentOrLog(out AbstractSEECity city) && city.ErosionSettings.ShowIssuesInCodeWindow) + if (go.TryGetComponentOrLog(out AbstractSEECity city)) { - MarkIssuesAsync(filename).Forget(); // initiate issue search in background + bool useDashboardIssues = city.ErosionSettings.ShowDashboardIssuesInCodeWindow; + bool useLspIssues = lspHandler != null && lspHandler.UseInCodeWindows; + MarkIssuesAsync(filename, useDashboardIssues, useLspIssues).Forget(); // initiate issue search in background } } return; @@ -423,49 +429,47 @@ bool TryGetLanguageOrLog(LSPHandler handler, out LSPLanguage language) /// with the collected tokens while marking all detected issues. /// /// The path to the file whose issues shall be marked. - private async UniTaskVoid MarkIssuesAsync(string path) + /// Whether to use issues from the Axivion Dashboard. + /// Whether to use issues from the LSP server. + private async UniTaskVoid MarkIssuesAsync(string path, bool useDashboardIssues, bool useLspIssues) { + if (!useDashboardIssues && !useLspIssues) + { + return; + } using (LoadingSpinner.ShowIndeterminate($"Loading issues for {Title}...")) { - string queryPath = Path.GetFileName(path); - List allIssues; - try + List allIssues = new(); + + if (useDashboardIssues) { - allIssues = new List(await DashboardRetriever.Instance.GetConfiguredIssuesAsync(fileFilter: $"\"*{queryPath}\"")); + allIssues.AddRange(await GetDashboardIssuesAsync(path)); } - catch (DashboardException e) + if (useLspIssues) { - ShowNotification.Error("Couldn't load issues", e.Message); - return; + allIssues.AddRange(GetLspIssues(path)); } - await UniTask.SwitchToThreadPool(); // don't interrupt main UI thread if (allIssues.Count == 0) { + Debug.Log($"No issues found for {path}"); return; } - const char pathSeparator = '/'; - // When there are different paths in the issue table, this implies that there are some files - // which aren't actually the one we're looking for (because we've only matched by filename so far). - // In this case, we'll gradually refine our results until this isn't the case anymore. - for (int skippedParts = path.Count(x => x == pathSeparator) - 2; !MatchingPaths(allIssues); skippedParts--) - { - Assert.IsTrue(path.Contains(pathSeparator)); - // Skip the first skippedParts parts, so that we query progressively larger parts. - queryPath = string.Join(pathSeparator.ToString(), path.Split(pathSeparator).Skip(skippedParts)); - allIssues.RemoveAll(x => !x.Entities.Select(e => e.Path).Any(p => p.EndsWith(queryPath))); - } + await UniTask.SwitchToThreadPool(); // don't interrupt main UI thread + string queryPath = Path.GetFileName(path); // Mapping from each line to the entities and issues contained therein. // Important: When an entity spans over multiple lines, it's split up into one entity per line. - Dictionary> entities = - allIssues.SelectMany(x => x.Entities.SelectMany(SplitUpIntoLines).Select(e => (entity: e, issue: x))) - .Where(x => x.entity.Path.EndsWith(queryPath)) - .OrderBy(x => x.entity.Line).GroupBy(x => x.entity.Line) - .ToDictionary(x => x.Key, x => x.ToList()); + IDictionary> issues = + allIssues.SelectMany(issue => issue.Occurrences + .SelectMany(e => e.Range.SplitIntoLines() + .Select(range => (path, range, issue)))) + .Where(x => x.path.EndsWith(queryPath)) + .OrderBy(x => x.range.StartLine).GroupBy(x => x.range.StartLine) + .ToDictionary(x => x.Key, x => x.Select(y => y.issue).ToList()); - EnterFromTokens(tokenList, entities); + EnterFromTokens(tokenList, issues); await UniTask.SwitchToMainThread(); @@ -481,23 +485,64 @@ private async UniTaskVoid MarkIssuesAsync(string path) ShowNotification.Error("File too big", "This file is too big to be displayed correctly."); } } - return; + } + + /// + /// Retrieves all issues for the given from the LSP server. + /// + /// The path of the file to get issues for. + /// A list of all issues for the given path. + private List GetLspIssues(string path) => + lspHandler.GetPublishedDiagnosticsForPath(path) + .SelectMany(x => x.Diagnostics) + .Select(x => new LSPIssue(path, x)) + .ToList(); + + /// + /// Retrieves all issues for the given from the Axivion Dashboard. + /// + /// The path of the file to get issues for. + /// A list of all issues for the given path. + private static async UniTask> GetDashboardIssuesAsync(string path) + { + string queryPath = Path.GetFileName(path); + List allIssues; + try + { + allIssues = new List(await DashboardRetriever.Instance.GetConfiguredIssuesAsync(fileFilter: $"\"*{queryPath}\"")); + } + catch (DashboardException e) + { + ShowNotification.Error("Couldn't load issues", e.Message); + return new List(); + } + + const char pathSeparator = '/'; + // When there are different paths in the issue table, this implies that there are some files + // which aren't actually the one we're looking for (because we've only matched by filename so far). + // In this case, we'll gradually refine our results until this isn't the case anymore. + for (int skippedParts = path.Count(x => x == pathSeparator) - 2; !AllMatchingPaths(allIssues); skippedParts--) + { + Assert.IsTrue(path.Contains(pathSeparator)); + // Skip the first skippedParts parts, so that we query progressively larger parts. + queryPath = string.Join(pathSeparator.ToString(), path.Split(pathSeparator).Skip(skippedParts)); + allIssues.RemoveAll(x => !x.Occurrences.Any(e => e.Path.EndsWith(queryPath))); + } + + return allIssues; // Returns true iff all issues are on the same path. - static bool MatchingPaths(ICollection issues) + static bool AllMatchingPaths(ICollection issues) { + if (!issues.Any()) + { + return true; + } // Every path in the first issue could be the "right" path, so we try them all. // If every issue has at least one path which matches that one, we can return true. - return issues.First().Entities.Select(e => e.Path) - .Any(path => issues.All(x => x.Entities.Any(e => e.Path == path))); + return issues.First().Occurrences.Select(e => e.Path) + .Any(path => issues.All(x => x.Occurrences.Any(e => e.Path == path))); } - - // Splits up a SourceCodeEntity into one entity per line it is set on (ranging from line to endLine). - // Each new entity will have a line attribute of the line it is split on and an endLine of null. - // If the input parameter has no endLine, an enumerable with this entity as its only value will be returned. - static IEnumerable SplitUpIntoLines(SourceCodeEntity entity) - => Enumerable.Range(entity.Line, entity.EndLine - entity.Line + 1 ?? 1) - .Select(l => new SourceCodeEntity(entity.Path, l, null, entity.Content)); } } } diff --git a/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs b/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs index 7ad349916d..69f61ba50b 100644 --- a/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs +++ b/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs @@ -15,6 +15,7 @@ using Microsoft.VisualStudio.Shared.VSCodeDebugProtocol.Messages; using SEE.Controls; using SEE.UI.DebugAdapterProtocol; +using UnityEngine.Assertions; namespace SEE.UI.Window.CodeWindow { @@ -155,23 +156,24 @@ private void SetupBreakpoints() protected override void UpdateDesktop() { - // Show issue info on click (on hover would be too expensive) - if (issueDictionary.Count != 0 && Input.GetMouseButtonDown(0)) + if (WindowSpaceManager.ManagerInstance[WindowSpaceManager.LocalPlayer].ActiveWindow == this) { - // Passing camera as null causes the screen space rather than world space camera to be used - int link = TMP_TextUtilities.FindIntersectingLink(textMesh, Input.mousePosition, null); - if (link != -1) + // Show issue info on click (on hover would be too expensive) + if (issueDictionary.Count != 0) { - char linkId = textMesh.textInfo.linkInfo[link].GetLinkID()[0]; - // Display tooltip containing all issue descriptions - UniTask.WhenAll(issueDictionary[linkId].Select(x => x.ToDisplayStringAsync())) - .ContinueWith(x => Tooltip.ActivateWith(string.Join("\n", x), Tooltip.AfterShownBehavior.HideUntilActivated)) - .Forget(); + // Passing camera as null causes the screen space rather than world space camera to be used + int link = TMP_TextUtilities.FindIntersectingLink(textMesh, Input.mousePosition, null); + if (link != -1) + { + char linkId = textMesh.textInfo.linkInfo[link].GetLinkID()[0]; + // Display tooltip containing all issue descriptions + UniTask.WhenAll(issueDictionary[linkId].Select(x => x.ToCodeWindowStringAsync())) + .ContinueWith(x => Tooltip.ActivateWith(string.Join('\n', x), Tooltip.AfterShownBehavior.HideUntilActivated)) + .Forget(); + } + return; } - } - if (WindowSpaceManager.ManagerInstance[WindowSpaceManager.LocalPlayer].ActiveWindow == this) - { // detecting word hovers int index = TMP_TextUtilities.FindIntersectingWord(textMesh, Input.mousePosition, null); TMP_WordInfo? hoveredWord = index >= 0 && index < textMesh.textInfo.wordCount ? textMesh.textInfo.wordInfo[index] : null; @@ -185,6 +187,9 @@ protected override void UpdateDesktop() } else if (!lastHoveredWord.Equals(hoveredWord)) { + Assert.IsTrue(hoveredWord != null); + Assert.IsTrue(lastHoveredWord != null); + OnWordHoverEnd?.Invoke(this, (TMP_WordInfo)lastHoveredWord); OnWordHoverBegin?.Invoke(this, (TMP_WordInfo)hoveredWord); } diff --git a/Assets/SEE/UI/Window/CodeWindow/DisplayableIssue.cs b/Assets/SEE/UI/Window/CodeWindow/DisplayableIssue.cs new file mode 100644 index 0000000000..5ed85aeb12 --- /dev/null +++ b/Assets/SEE/UI/Window/CodeWindow/DisplayableIssue.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Cysharp.Threading.Tasks; +using SEE.DataModel.DG; + +namespace SEE.UI.Window.CodeWindow +{ + /// + /// A code issue or diagnostic that can be displayed in code windows. + /// + public interface IDisplayableIssue + { + /// + /// Returns the message of the issue as a string. + /// This message may need to be retrieved asynchronously. + /// + /// The message of the issue. + UniTask ToDisplayStringAsync(); + + /// + /// The source of the issue, for example, "LSP" or "Axivion". + /// + string Source { get; } + + /// + /// All occurrences of this issue in the code. + /// + IEnumerable<(string Path, Range Range)> Occurrences { get; } + + /// + /// A list of rich tags that should be used to render the issue. + /// These will include the tag separators. For example, an entry here might be ]]>. + /// + IList RichTags { get; } + + /// + /// The opening rich tags. Should be put in front of any occurrence of the issue. + /// + string OpeningRichTags => string.Join("", RichTags); + + /// + /// The closing rich tags. Should be put after any occurrence of the issue. + /// + string ClosingRichTags => string.Join("", RichTags.Reverse().Select(ToClosingTag)); + + /// + /// Converts the given to a closing TextMeshPro tag. + /// + /// The opening tag to convert. + /// The closing tag that corresponds to the given . + private static string ToClosingTag(string openingTag) => new Regex(@"<([^\s=]*)[ =]?.*>").Replace(openingTag, ""); + + /// + /// Whether the rich tags contain any color tags. + /// + bool HasColorTags => RichTags.Any(t => t.StartsWith(" + /// Returns a string describing the issue that can be displayed in a code window. + /// Hence, this may contain rich text tags. + /// + /// A string describing the issue that can be displayed in a code window. + public async UniTask ToCodeWindowStringAsync() + { + string message = await ToDisplayStringAsync(); + return $"{message}\n\nSource: {Source}"; + } + + /// + /// Returns the range of characters (the end character being exclusive) that contain an + /// occurrence of this issue in the given line. + /// If such an occurrence either does not exist, or is ambiguous, this method returns null. + /// + /// The path of the file that contains the line. + /// The line number of the line that contains the issue. + /// The content of the line that contains the issue. + /// The range of characters that contain an occurrence of this issue in the given line, + /// or null if such an occurrence does not exist or is ambiguous. + (int startCharacter, int endCharacter)? GetCharacterRangeForLine(string path, int lineNumber, string line) + { + return Occurrences + .Where(o => o.Range.HasCharacter) + .Where(o => o.Path == path && o.Range.Overlaps(lineNumber)) + .Select<(string Path, Range Range), (int, int)?>(o => + { + if (o.Range.StartLine == o.Range.EndLine) + { + // We are on the only line of this issue. + return (o.Range.StartCharacter!.Value, o.Range.EndCharacter!.Value); + } + else if (lineNumber == o.Range.StartLine) + { + // We are on the first line of this issue. + return (o.Range.StartCharacter!.Value, line.Length + 1); + } + else if (lineNumber == o.Range.EndLine) + { + // We are on the last line of this issue. + return (0, o.Range.EndCharacter!.Value); + } + else + { + // We are on a line in between the first and last line of this issue. + return (0, line.Length + 1); + } + }) + .DefaultIfEmpty(null) + .First(); + } + } +} diff --git a/Assets/SEE/UI/Window/CodeWindow/DisplayableIssue.cs.meta b/Assets/SEE/UI/Window/CodeWindow/DisplayableIssue.cs.meta new file mode 100644 index 0000000000..75ea9f3f34 --- /dev/null +++ b/Assets/SEE/UI/Window/CodeWindow/DisplayableIssue.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 29746451aa3941e4a54e4d078ce8c1a1 +timeCreated: 1720128171 \ No newline at end of file diff --git a/Assets/SEE/UI/Window/DesktopWindowSpace.cs b/Assets/SEE/UI/Window/DesktopWindowSpace.cs index d5769bd6af..3b3a22c93f 100644 --- a/Assets/SEE/UI/Window/DesktopWindowSpace.cs +++ b/Assets/SEE/UI/Window/DesktopWindowSpace.cs @@ -188,8 +188,9 @@ private void InitializePanel() } panel = PanelUtils.CreatePanelFor((RectTransform)windows[0].Window.transform, panelsCanvas); // When the active tab *on this panel* is changed, we invoke the corresponding event - PanelNotificationCenter.OnActiveTabChanged += ChangeActiveTab; PanelNotificationCenter.OnPanelClosed += ClosePanel; + PanelNotificationCenter.OnActiveTabChanged += ChangeActiveTab; + return; void ChangeActiveTab(PanelTab tab) { diff --git a/Assets/SEE/Utils/CollectionExtensions.cs b/Assets/SEE/Utils/CollectionExtensions.cs index 45e07d059b..d4c64ef636 100644 --- a/Assets/SEE/Utils/CollectionExtensions.cs +++ b/Assets/SEE/Utils/CollectionExtensions.cs @@ -29,15 +29,15 @@ public static void Toggle(this ISet set, T element) /// /// Gets the value for the given from the given . /// If the key is not present in the dictionary, the given - /// will be added to the dictionary and returned. + /// will be evaluated and its result added to the dictionary and returned. /// /// The dictionary from which the value shall be retrieved. /// The key for which the value shall be retrieved. - /// The default value which shall be added to the dictionary if the key is not present. + /// A lambda returning the default value which shall be added to the dictionary if the key is not present. /// The type of the keys in the dictionary. /// The type of the values in the dictionary. /// The value for the given from the given . - public static V GetOrAdd(this IDictionary dict, K key, V defaultValue) + public static V GetOrAdd(this IDictionary dict, K key, Func defaultValue) { if (dict.TryGetValue(key, out V value)) { @@ -45,7 +45,7 @@ public static V GetOrAdd(this IDictionary dict, K key, V defaultValue } else { - return dict[key] = defaultValue; + return dict[key] = defaultValue(); } } diff --git a/Assets/SEE/Utils/DefaultDictionary.cs b/Assets/SEE/Utils/DefaultDictionary.cs index 129b29980a..4368445a7c 100644 --- a/Assets/SEE/Utils/DefaultDictionary.cs +++ b/Assets/SEE/Utils/DefaultDictionary.cs @@ -13,7 +13,7 @@ namespace SEE.Utils { public new V this[K key] { - get => this.GetOrAdd(key, new V()); + get => this.GetOrAdd(key, () => new V()); set => base[key] = value; } } diff --git a/Assets/SEETests/TestConfigIO.cs b/Assets/SEETests/TestConfigIO.cs index fcdd9dfd40..00dd966a59 100644 --- a/Assets/SEETests/TestConfigIO.cs +++ b/Assets/SEETests/TestConfigIO.cs @@ -966,7 +966,7 @@ private static void WipeOutErosionSettings(AbstractSEECity city) { city.ErosionSettings.ShowInnerErosions = !city.ErosionSettings.ShowInnerErosions; city.ErosionSettings.ShowLeafErosions = !city.ErosionSettings.ShowLeafErosions; - city.ErosionSettings.ShowIssuesInCodeWindow = !city.ErosionSettings.ShowIssuesInCodeWindow; + city.ErosionSettings.ShowDashboardIssuesInCodeWindow = !city.ErosionSettings.ShowDashboardIssuesInCodeWindow; city.ErosionSettings.ErosionScalingFactor++; city.ErosionSettings.StyleIssue = "X"; @@ -994,7 +994,7 @@ private static void AreEqualErosionSettings(ErosionAttributes expected, ErosionA { Assert.AreEqual(expected.ShowInnerErosions, actual.ShowInnerErosions); Assert.AreEqual(expected.ShowLeafErosions, actual.ShowLeafErosions); - Assert.AreEqual(expected.ShowIssuesInCodeWindow, actual.ShowIssuesInCodeWindow); + Assert.AreEqual(expected.ShowDashboardIssuesInCodeWindow, actual.ShowDashboardIssuesInCodeWindow); Assert.AreEqual(expected.ErosionScalingFactor, actual.ErosionScalingFactor); Assert.AreEqual(expected.StyleIssue, actual.StyleIssue); From 3ec962aa4019a10cb45e45f1f7ef15fb6f7d281c Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Thu, 18 Jul 2024 19:20:57 +0200 Subject: [PATCH 04/23] Update to Unity 2022.3.38f1 --- .../Examples/Scenes/SALSA-basic3D-boxhead.unity | 4 ++-- Axivion/axivion-jenkins.bat | 2 +- ProjectSettings/ProjectVersion.txt | 4 ++-- README.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Assets/Plugins/Crazy Minnow Studio/Examples/Scenes/SALSA-basic3D-boxhead.unity b/Assets/Plugins/Crazy Minnow Studio/Examples/Scenes/SALSA-basic3D-boxhead.unity index b341152f1f..e77442f267 100644 --- a/Assets/Plugins/Crazy Minnow Studio/Examples/Scenes/SALSA-basic3D-boxhead.unity +++ b/Assets/Plugins/Crazy Minnow Studio/Examples/Scenes/SALSA-basic3D-boxhead.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d5b5f035c5272eee77cdafb12c97f11f87ae7a9a4d5e0e0d74cc7b962daefa0 -size 121348 +oid sha256:c2a972840e32219bdb153028f1e9627ada544af6f15b32dcff28d55fa7958732 +size 83085 diff --git a/Axivion/axivion-jenkins.bat b/Axivion/axivion-jenkins.bat index 1e3beb4a72..7fe5373d96 100644 --- a/Axivion/axivion-jenkins.bat +++ b/Axivion/axivion-jenkins.bat @@ -107,7 +107,7 @@ if "%AXIVION_DASHBOARD_URL%"=="" ( ) if "%UNITY%"=="" ( - set "UNITY=C:\Program Files\Unity\Hub\Editor\2022.3.37f1" + set "UNITY=C:\Program Files\Unity\Hub\Editor\2022.3.38f1" ) if not exist "%UNITY%" ( diff --git a/ProjectSettings/ProjectVersion.txt b/ProjectSettings/ProjectVersion.txt index b12da398a4..793a36e15f 100644 --- a/ProjectSettings/ProjectVersion.txt +++ b/ProjectSettings/ProjectVersion.txt @@ -1,2 +1,2 @@ -m_EditorVersion: 2022.3.37f1 -m_EditorVersionWithRevision: 2022.3.37f1 (340ba89e4c23) +m_EditorVersion: 2022.3.38f1 +m_EditorVersionWithRevision: 2022.3.38f1 (c5d5a7410213) diff --git a/README.md b/README.md index c7dad28611..2ee8b9a1a3 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Tests](https://github.com/uni-bremen-agst/SEE/actions/workflows/main.yml/badge.svg)](https://github.com/uni-bremen-agst/SEE/actions/workflows/main.yml) SEE visualizes hierarchical dependency graphs of software in 3D/VR based on the city metaphor. -The underlying game engine is Unity 3D (version 2022.3.37f1). +The underlying game engine is Unity 3D (version 2022.3.38f1). ![Screenshot of SEE](Screenshot.png) From 3bf138727c6ae05eb59ba0df2036e630c128ee23 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Fri, 19 Jul 2024 18:19:57 +0200 Subject: [PATCH 05/23] Implement LSP hover info for code windows #686 --- .../Prefabs/UI/CodeWindowContent.prefab | 4 +- Assets/SEE/DataModel/DG/IO/LSPImporter.cs | 59 +++----------- .../DebugAdapterProtocolSession.cs | 22 +++--- .../DebugAdapterProtocolSessionEvents.cs | 39 +++++----- Assets/SEE/UI/Tooltip.cs | 6 ++ Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs | 1 + .../UI/Window/CodeWindow/CodeWindowInput.cs | 50 ++++++++---- .../UI/Window/CodeWindow/DesktopCodeWindow.cs | 77 +++++++++++++++---- Assets/SEE/Utils/MarkdownConverter.cs | 64 +++++++++++++++ Assets/SEE/Utils/MarkdownConverter.cs.meta | 3 + 10 files changed, 215 insertions(+), 110 deletions(-) create mode 100644 Assets/SEE/Utils/MarkdownConverter.cs create mode 100644 Assets/SEE/Utils/MarkdownConverter.cs.meta diff --git a/Assets/Resources/Prefabs/UI/CodeWindowContent.prefab b/Assets/Resources/Prefabs/UI/CodeWindowContent.prefab index faffcf96bd..d64fb9ec78 100644 --- a/Assets/Resources/Prefabs/UI/CodeWindowContent.prefab +++ b/Assets/Resources/Prefabs/UI/CodeWindowContent.prefab @@ -684,7 +684,7 @@ MonoBehaviour: m_lineSpacingMax: 0 m_paragraphSpacing: 0 m_charWidthMaxAdj: 0 - m_enableWordWrapping: 1 + m_enableWordWrapping: 0 m_wordWrappingRatios: 0.413 m_overflowMode: 0 m_linkedTextComponent: {fileID: 0} @@ -722,7 +722,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} m_Name: m_EditorClassIdentifier: - m_HorizontalFit: 0 + m_HorizontalFit: 2 m_VerticalFit: 2 --- !u!1 &8823517661321162182 GameObject: diff --git a/Assets/SEE/DataModel/DG/IO/LSPImporter.cs b/Assets/SEE/DataModel/DG/IO/LSPImporter.cs index 80584373d4..90d18822e5 100644 --- a/Assets/SEE/DataModel/DG/IO/LSPImporter.cs +++ b/Assets/SEE/DataModel/DG/IO/LSPImporter.cs @@ -6,7 +6,6 @@ using Cysharp.Threading.Tasks; using Markdig; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -using OmniSharp.Extensions.LanguageServer.Server; using SEE.Tools; using SEE.Tools.LSP; using SEE.Utils; @@ -394,7 +393,11 @@ private async UniTask HandleCallHierarchyAsync(Node node, Graph graph, Cancellat { throw new OperationCanceledException(); } - Node targetNode = FindNodesByLocation(item.Uri.Path, Range.FromLspRange(item.Range)).First(); + Node targetNode = FindNodesByLocation(item.Uri.Path, Range.FromLspRange(item.Range)).FirstOrDefault(); + if (targetNode == null) + { + continue; + } Edge edge = AddEdge(node, targetNode, LSP.Call, false, graph); edge.SetRange(SelectionRangeAttribute, Range.FromLspRange(item.SelectionRange)); } @@ -422,7 +425,11 @@ private async UniTask HandleTypeHierarchyAsync(Node node, Graph graph, Cancellat { throw new OperationCanceledException(); } - Node targetNode = FindNodesByLocation(item.Uri.Path, Range.FromLspRange(item.Range)).First(); + Node targetNode = FindNodesByLocation(item.Uri.Path, Range.FromLspRange(item.Range)).FirstOrDefault(); + if (targetNode == null) + { + continue; + } Edge edge = AddEdge(node, targetNode, LSP.Extend, false, graph); edge.SetRange(SelectionRangeAttribute, Range.FromLspRange(item.SelectionRange)); } @@ -494,7 +501,7 @@ private async UniTask AddSymbolNodeAsync(DocumentSymbol symbol, string path, Gra Hover hover = await Handler.HoverAsync(path, node.SourceLine - 1 ?? 0, node.SourceColumn - 1 ?? 0); if (hover != null) { - node.SetString("HoverText", MarkupToRichText(hover.Contents)); + node.SetString("HoverText", hover.Contents.ToRichText()); } } @@ -524,50 +531,6 @@ private async UniTask AddSymbolNodeAsync(DocumentSymbol symbol, string path, Gra } } - /// - /// Converts the given to TextMeshPro-compatible rich text. - /// - /// The content to convert. - /// The converted rich text. - private static string MarkupToRichText(MarkedStringsOrMarkupContent content) - { - string markdown; - if (content.HasMarkupContent) - { - MarkupContent markup = content.MarkupContent!; - switch (markup.Kind) - { - case MarkupKind.PlainText: return $"{markup.Value}"; - case MarkupKind.Markdown: - markdown = markup.Value; - break; - default: - Debug.LogError($"Unsupported markup kind: {markup.Kind}"); - return string.Empty; - } - } - else - { - // This is technically deprecated, but we still need to support it, - // since some language servers still use it. - Container strings = content.MarkedStrings!; - markdown = string.Join("\n", strings.Select(x => - { - if (x.Language != null) - { - return $"```{x.Language}\n{x.Value}\n```"; - } - else - { - return x.Value; - } - })); - } - - // TODO (#728): Parse markdown to TextMeshPro rich text (custom MarkDig parser). - return Markdown.ToPlainText(markdown); - } - /// /// Adds a node for the given to the given . /// If the node already exists, it is returned immediately. diff --git a/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSession.cs b/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSession.cs index d11acae1cb..dd4249a142 100644 --- a/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSession.cs +++ b/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSession.cs @@ -336,11 +336,11 @@ private void OnDestroy() { Destroyer.Destroy(controls); } - if (adapterProcess != null && !adapterProcess.HasExited) + if (adapterProcess is { HasExited: false }) { adapterProcess.Kill(); } - if (adapterHost != null && adapterHost.IsRunning) + if (adapterHost is { IsRunning: true }) { adapterHost.Stop(); } @@ -349,12 +349,12 @@ private void OnDestroy() /// /// Creates the process for the debug adapter. /// - /// Whether the creation was sucessful. + /// Whether the creation was successful. private bool CreateAdapterProcess() { adapterProcess = new Process { - StartInfo = new ProcessStartInfo() + StartInfo = new ProcessStartInfo { FileName = Adapter.AdapterFileName, Arguments = Adapter.AdapterArguments, @@ -371,8 +371,8 @@ private bool CreateAdapterProcess() }, EnableRaisingEvents = true }; - adapterProcess.Exited += (_, args) => ConsoleWindow.AddMessage($"Process: Exited! ({(!adapterProcess.HasExited ? adapterProcess.ProcessName : null)})"); - adapterProcess.Disposed += (_, args) => ConsoleWindow.AddMessage($"Process: Exited! ({(!adapterProcess.HasExited ? adapterProcess.ProcessName : null)})"); + adapterProcess.Exited += (_, _) => ConsoleWindow.AddMessage($"Process: Exited! ({(!adapterProcess.HasExited ? adapterProcess.ProcessName : null)})"); + adapterProcess.Disposed += (_, _) => ConsoleWindow.AddMessage($"Process: Exited! ({(!adapterProcess.HasExited ? adapterProcess.ProcessName : null)})"); adapterProcess.OutputDataReceived += (_, args) => ConsoleWindow.AddMessage($"Process: OutputDataReceived! ({adapterProcess.ProcessName})\n\t{args.Data}"); adapterProcess.ErrorDataReceived += (_, args) => { @@ -410,7 +410,7 @@ private bool CreateAdapterProcess() private bool CreateAdapterHost() { adapterHost = new DebugProtocolHost(adapterProcess.StandardInput.BaseStream, adapterProcess.StandardOutput.BaseStream); - adapterHost.DispatcherError += (sender, args) => + adapterHost.DispatcherError += (_, args) => { string message = $"DispatcherError - {args.Exception}"; ConsoleWindow.AddMessage(message + "\n", "Adapter", "Error"); @@ -451,7 +451,7 @@ private void UpdateStackFrames() return; } - stackFrames = adapterHost.SendRequestSync(new StackTraceRequest() { ThreadId = MainThread.Id }).StackFrames; + stackFrames = adapterHost.SendRequestSync(new StackTraceRequest { ThreadId = MainThread.Id }).StackFrames; } @@ -474,7 +474,7 @@ private void UpdateVariables() foreach (StackFrame stackFrame in stackFrames) { - List stackScopes = adapterHost.SendRequestSync(new ScopesRequest() { FrameId = stackFrame.Id }).Scopes; + List stackScopes = adapterHost.SendRequestSync(new ScopesRequest { FrameId = stackFrame.Id }).Scopes; Dictionary> stackVariables = stackScopes.ToDictionary(scope => scope, scope => RetrieveNestedVariables(scope.VariablesReference)); threadVariables.Add(stackFrame, stackVariables); } @@ -499,7 +499,7 @@ private List RetrieveNestedVariables(int variablesReference) { return new(); } - return adapterHost.SendRequestSync(new VariablesRequest() { VariablesReference = variablesReference }).Variables; + return adapterHost.SendRequestSync(new VariablesRequest { VariablesReference = variablesReference }).Variables; } /// @@ -512,7 +512,7 @@ private string RetrieveVariableValue(Variable variable) { if (IsRunning && variable.EvaluateName != null) { - EvaluateResponse value = adapterHost.SendRequestSync(new EvaluateRequest() + EvaluateResponse value = adapterHost.SendRequestSync(new EvaluateRequest { Expression = variable.EvaluateName, FrameId = IsRunning ? null : StackFrame.Id diff --git a/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSessionEvents.cs b/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSessionEvents.cs index 4760de1f89..c45d0113d2 100644 --- a/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSessionEvents.cs +++ b/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSessionEvents.cs @@ -25,16 +25,16 @@ private void OnBreakpointsChanged(string path, int line) { actions.Enqueue(() => { - adapterHost.SendRequest(new SetBreakpointsRequest() + adapterHost.SendRequest(new SetBreakpointsRequest { - Source = new Source() { Path = path, Name = path }, + Source = new Source { Path = path, Name = path }, Breakpoints = DebugBreakpointManager.Breakpoints[path].Values.ToList(), }, _ => { }); }); } /// - /// Handles the begining of hovering a word. + /// Handles the beginning of hovering a word. /// /// The code window containing the hovered word. /// The info of the hovered word. @@ -66,11 +66,11 @@ private void UpdateHoverTooltip() return; } - string expression = ((TMP_WordInfo)hoveredWord).GetWord(); + string expression = hoveredWord.Value.GetWord(); try { - EvaluateResponse result = adapterHost.SendRequestSync(new EvaluateRequest() + EvaluateResponse result = adapterHost.SendRequestSync(new EvaluateRequest { Expression = expression, Context = capabilities.SupportsEvaluateForHovers == true ? EvaluateArguments.ContextValue.Hover : null, @@ -133,14 +133,14 @@ private void OnInitializedEvent(InitializedEvent initializedEvent) adapterHost.SendRequest(Adapter.GetLaunchRequest(), _ => IsRunning = true); foreach ((string path, Dictionary breakpoints) in DebugBreakpointManager.Breakpoints) { - adapterHost.SendRequest(new SetBreakpointsRequest() + adapterHost.SendRequest(new SetBreakpointsRequest { - Source = new Source() { Path = path, Name = path }, + Source = new Source { Path = path, Name = path }, Breakpoints = breakpoints.Values.ToList(), }, _ => { }); } - adapterHost.SendRequest(new SetFunctionBreakpointsRequest() { Breakpoints = new() }, _ => { }); - adapterHost.SendRequest(new SetExceptionBreakpointsRequest() { Filters = new() }, _ => { }); + adapterHost.SendRequest(new SetFunctionBreakpointsRequest { Breakpoints = new() }, _ => { }); + adapterHost.SendRequest(new SetExceptionBreakpointsRequest { Filters = new() }, _ => { }); if (capabilities.SupportsConfigurationDoneRequest == true) { adapterHost.SendRequest(new ConfigurationDoneRequest(), _ => { }); @@ -227,7 +227,7 @@ private void OnStoppedEvent(StoppedEvent stoppedEvent) return; } - ExceptionInfoResponse exceptionInfo = adapterHost.SendRequestSync(new ExceptionInfoRequest() + ExceptionInfoResponse exceptionInfo = adapterHost.SendRequestSync(new ExceptionInfoRequest { ThreadId = MainThread.Id, }); @@ -358,7 +358,7 @@ private void OnConsoleInput(string text) /// /// Queues a continue request. /// - void OnContinue() + private void OnContinue() { actions.Enqueue(() => { @@ -373,7 +373,7 @@ void OnContinue() /// /// Queues a pause request. /// - void OnPause() + private void OnPause() { actions.Enqueue(() => { @@ -388,7 +388,7 @@ void OnPause() /// /// Queues a reverse continue request. /// - void OnReverseContinue() + private void OnReverseContinue() { actions.Enqueue(() => { @@ -403,7 +403,7 @@ void OnReverseContinue() /// /// Queues a next request. /// - void OnNext() + private void OnNext() { actions.Enqueue(() => { @@ -418,7 +418,7 @@ void OnNext() /// /// Queues a step back request. /// - void OnStepBack() + private void OnStepBack() { actions.Enqueue(() => { @@ -433,7 +433,7 @@ void OnStepBack() /// /// Queues a step in request. /// - void OnStepIn() + private void OnStepIn() { actions.Enqueue(() => { @@ -448,7 +448,7 @@ void OnStepIn() /// /// Queues a step out request. /// - void OnStepOut() + private void OnStepOut() { actions.Enqueue(() => { @@ -463,7 +463,7 @@ void OnStepOut() /// /// Queues a restart request. /// - void OnRestart() + private void OnRestart() { actions.Enqueue(() => { @@ -474,7 +474,7 @@ void OnRestart() /// /// Queues a terminate request. /// - void OnStop() + private void OnStop() { actions.Enqueue(() => { @@ -487,6 +487,7 @@ void OnStop() Disconnect(); } }); + return; // Tries to stop the debuggee gracefully. void Terminate() diff --git a/Assets/SEE/UI/Tooltip.cs b/Assets/SEE/UI/Tooltip.cs index 7da0438271..55ad73f894 100644 --- a/Assets/SEE/UI/Tooltip.cs +++ b/Assets/SEE/UI/Tooltip.cs @@ -221,6 +221,12 @@ public static void ActivateWith(string text, AfterShownBehavior afterShownBehavi Instance.ChangeText(text, afterShownBehavior); } + /// + /// Whether the tooltip is currently active. + /// Note that "active" does not necessarily mean that the tooltip is currently visible. + /// + public static bool IsActivated => Instance.text != null; + /// /// Will hide the tooltip by fading it out if it's currently visible. /// If has been called prior to this and is active, it will be halted. diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs b/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs index ca25e12cfd..2ffdb1f66c 100644 --- a/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs @@ -211,6 +211,7 @@ private float ImmediateVisibleLine else { scrollRect.verticalNormalizedPosition = 1 - value / (lines - 1 - excessLines); + scrollRect.horizontalNormalizedPosition = 0; } } } diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs b/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs index 957bc749db..aff32a9c4b 100644 --- a/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs @@ -45,6 +45,14 @@ public partial class CodeWindow /// private List tokenList; + /// + /// A list of starting offsets of each line in the code window (without rich tags), sorted in ascending order. + /// + /// In other words, this contains the character indices of newlines in the rendered text (the text without + /// the rich tags in them, but with the line numbers at the beginning of each line). + /// + private List CodeWindowOffsets { get; } = new(); + /// /// Characters representing newlines. /// Note that newlines may also consist of aggregations of this set (e.g. "\r\n"). @@ -60,7 +68,8 @@ public partial class CodeWindow /// If you wish to use such issues, split the entities up into one per line (see ). /// /// If is null. - private void EnterFromTokens(IEnumerable tokens, IDictionary> issues = null) + private void EnterFromTokens(IEnumerable tokens, + IDictionary> issues = null) { if (tokens == null) { @@ -82,9 +91,16 @@ private void EnterFromTokens(IEnumerable tokens, IDictionary token.Text.Count(x => x == '\n')); // Needed padding is the number of lines, because the line number will be at most this long. neededPadding = assumedLines.ToString().Length; - text = $"{string.Join("", Enumerable.Repeat(" ", neededPadding - 1))}1 "; - int lineNumber = 2; // Line number we'll write down next + + CodeWindowOffsets.Clear(); + // The first line starts at the beginning of the text after the line number. + CodeWindowOffsets.Add(0); + + // Line number we'll write down next + int lineNumber = 2; + // Offset of the current character in the text (excluding rich tags) + int characterOffset = neededPadding + 1; // + 1 for the space after the line number // The issue that we're currently marking, if any. IDisplayableIssue currentlyMarking = null; // We need reference equality here. @@ -94,11 +110,11 @@ private void EnterFromTokens(IEnumerable tokens, IDictionary tokens, IDictionary{theLineNumber} "; + characterOffset += neededPadding + 1; if (issues?.ContainsKey(theLineNumber) ?? false) { @@ -134,8 +155,7 @@ void AppendNewline(ref int theLineNumber, ref string text, int padding, SEEToken } // Handles a token which may contain newlines and adds its syntax-highlighted content to the code window. - // Returns the new line number. - int HandleToken(SEEToken token) + void HandleToken(SEEToken token) { string[] newlineStrings = newlineCharacters.Select(x => x.ToString()).Concat(new[] { @@ -150,7 +170,7 @@ int HandleToken(SEEToken token) // Any entry after the first is on a separate line. if (!firstRun) { - AppendNewline(ref lineNumber, ref text, neededPadding, token); + AppendNewline(ref lineNumber, ref text, token); } // Mark any potential issue @@ -188,7 +208,9 @@ int HandleToken(SEEToken token) { // We just copy the whitespace verbatim, no need to even color it. // Note: We have to assume that whitespace will not interfere with TMP's XML syntax. - text += line.Replace("\t", new string(' ', token.Language.TabWidth)); + string replaced = line.Replace("\t", new string(' ', token.Language.TabWidth)); + text += replaced; + characterOffset += replaced.Length; } else { @@ -202,7 +224,9 @@ int HandleToken(SEEToken token) { text += $""; } - text += $"{line.Replace("/noparse", "")}"; + string replaced = line.Replace("", @"<\noparse>"); + text += $"{replaced}"; + characterOffset += replaced.Length; if (currentlyMarking is not { HasColorTags: true }) { text += ""; @@ -216,8 +240,6 @@ int HandleToken(SEEToken token) firstRun = false; } - - return lineNumber; } // Begins marking a new issue segment starting with the given token. diff --git a/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs b/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs index 69f61ba50b..d8a172a5b5 100644 --- a/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs +++ b/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs @@ -13,6 +13,7 @@ using UnityEngine.UI; using System.Collections.Generic; using Microsoft.VisualStudio.Shared.VSCodeDebugProtocol.Messages; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; using SEE.Controls; using SEE.UI.DebugAdapterProtocol; using UnityEngine.Assertions; @@ -65,7 +66,8 @@ protected override void StartDesktop() DebugBreakpointManager.OnBreakpointRemoved += OnBreakpointRemoved; Transform temp = SceneQueries.GetCodeCity(transform); - if (temp && temp.gameObject.TryGetComponentOrLog(out AbstractSEECity city)) { + if (temp && temp.gameObject.TryGetComponentOrLog(out AbstractSEECity city)) + { // Get button for IDE interaction and register events. Window.transform.Find("Dragger/IDEButton").gameObject.GetComponent - public class NestedMenuEntry : MenuEntry where T : MenuEntry + public record NestedMenuEntry : MenuEntry where T : MenuEntry { /// /// The menu entries which shall fill the menu when selecting this entry. @@ -26,9 +26,8 @@ public class NestedMenuEntry : MenuEntry where T : MenuEntry /// Whether this entry should be enabled on creation. /// The icon which shall be displayed alongside this entry. /// If is null. - public NestedMenuEntry(IEnumerable innerEntries, string title, string description = null, - Color entryColor = default, bool enabled = true, Sprite icon = null) : - base(() => { }, () => { }, title, description, entryColor, enabled, icon) + public NestedMenuEntry(IEnumerable innerEntries, string title, string description = null, Color entryColor = default, bool enabled = true, Sprite icon = null) : + base(() => { }, title, () => { }, description, entryColor, enabled, icon) { InnerEntries = innerEntries?.ToList() ?? throw new ArgumentNullException(nameof(innerEntries)); } diff --git a/Assets/SEE/UI/Menu/SelectionMenu.cs b/Assets/SEE/UI/Menu/SelectionMenu.cs index a269161a8b..a50f51c5db 100644 --- a/Assets/SEE/UI/Menu/SelectionMenu.cs +++ b/Assets/SEE/UI/Menu/SelectionMenu.cs @@ -34,7 +34,7 @@ public T ActiveEntry activeEntry = value; if (oldActiveEntry != null) { - oldActiveEntry.UnselectAction(); + oldActiveEntry.UnselectAction?.Invoke(); OnEntryUnselected?.Invoke(oldActiveEntry); } OnActiveEntryChanged?.Invoke(); diff --git a/Assets/SEE/UI/Menu/SimpleListMenu.cs b/Assets/SEE/UI/Menu/SimpleListMenu.cs index f8a8a4ec4a..9d47491717 100644 --- a/Assets/SEE/UI/Menu/SimpleListMenu.cs +++ b/Assets/SEE/UI/Menu/SimpleListMenu.cs @@ -145,26 +145,26 @@ protected override void HandleKeyword(PhraseRecognizedEventArgs args) /// /// Triggers when was changed. /// - public event UnityAction OnAllowNoSelectionChanged; + public event Action OnAllowNoSelectionChanged; /// /// Triggers when was changed. /// - public event UnityAction OnHideAfterSelectionChanged; + public event Action OnHideAfterSelectionChanged; /// /// Triggers when an entry was added. () /// - public event UnityAction OnEntryAdded; + public event Action OnEntryAdded; /// /// Triggers when an entry was removed. () /// - public event UnityAction OnEntryRemoved; + public event Action OnEntryRemoved; /// /// Triggers when an entry was selected. () /// - public event UnityAction OnEntrySelected; + public event Action OnEntrySelected; } } diff --git a/Assets/SEE/UI/OpeningDialog.cs b/Assets/SEE/UI/OpeningDialog.cs index 56b8f27cfb..aca2444294 100644 --- a/Assets/SEE/UI/OpeningDialog.cs +++ b/Assets/SEE/UI/OpeningDialog.cs @@ -53,34 +53,30 @@ private IList SelectionEntries() return new List { - new(selectAction: StartHost, - unselectAction: null, - title: "Host", - description: "Starts a server and local client process.", - entryColor: NextColor(), - icon: Resources.Load("Icons/Host")), + new(SelectAction: StartHost, + Title: "Host", + Description: "Starts a server and local client process.", + EntryColor: NextColor(), + Icon: Resources.Load("Icons/Host")), - new(selectAction: StartClient, - unselectAction: null, - title: "Client", - description: "Starts a local client connection to a server.", - entryColor: NextColor(), - icon: Resources.Load("Icons/Client")), + new(SelectAction: StartClient, + Title: "Client", + Description: "Starts a local client connection to a server.", + EntryColor: NextColor(), + Icon: Resources.Load("Icons/Client")), #if ENABLE_VR - new(selectAction: ToggleEnvironment, - unselectAction: null, - title: "Toggle Desktop/VR", - description: "Toggles between desktop and VR hardware.", - entryColor: NextColor(), - icon: Resources.Load("Icons/Client")), + new(SelectAction: ToggleEnvironment, + Title: "Toggle Desktop/VR", + Description: "Toggles between desktop and VR hardware.", + EntryColor: NextColor(), + Icon: Resources.Load("Icons/Client")), #endif - new(selectAction: Settings, - unselectAction: null, - title: "Settings", - description: "Allows to set additional network settings.", - entryColor: Color.gray, - icon: Resources.Load("Icons/Settings")), + new(SelectAction: Settings, + Title: "Settings", + Description: "Allows to set additional network settings.", + EntryColor: Color.gray, + Icon: Resources.Load("Icons/Settings")), }; Color NextColor() diff --git a/Assets/SEE/UI/RuntimeConfigMenu/RuntimeTabMenu.cs b/Assets/SEE/UI/RuntimeConfigMenu/RuntimeTabMenu.cs index 4d2964f799..643b816d4a 100644 --- a/Assets/SEE/UI/RuntimeConfigMenu/RuntimeTabMenu.cs +++ b/Assets/SEE/UI/RuntimeConfigMenu/RuntimeTabMenu.cs @@ -263,7 +263,7 @@ string GetButtonGroup(MemberInfo memberInfo) => (memberInfo.GetCustomAttributes().OfType().FirstOrDefault() ?? new RuntimeButtonAttribute(null, null)).Name; - // ordered depending if a setting is primitive or has nested settings + // ordered depending on if a setting is primitive or has nested settings bool SortIsNotNested(MemberInfo memberInfo) { object value; @@ -388,12 +388,11 @@ private GameObject CreateOrGetViewGameObject(IEnumerable attributes) if (entry == null) { entry = new MenuEntry( - () => { }, - () => { }, - tabName, - $"Settings for {tabName}", - GetColorForTab(), - Resources.Load("Materials/Charts/MoveIcon") + SelectAction: () => { }, + Title: tabName, + Description: $"Settings for {tabName}", + EntryColor: GetColorForTab(), + Icon: Resources.Load("Materials/Charts/MoveIcon") ); AddEntry(entry); } diff --git a/Assets/SEEPlayModeTests/TestNestedMenu.cs b/Assets/SEEPlayModeTests/TestNestedMenu.cs index ee0ae46b8a..146deb08a6 100644 --- a/Assets/SEEPlayModeTests/TestNestedMenu.cs +++ b/Assets/SEEPlayModeTests/TestNestedMenu.cs @@ -1,8 +1,7 @@ -using NUnit.Framework; -using System.Collections; +using System.Collections; using System.Collections.Generic; +using NUnit.Framework; using UnityEngine; -using UnityEngine.Events; using UnityEngine.TestTools; namespace SEE.UI.Menu @@ -53,7 +52,7 @@ internal class TestNestedMenu : TestMenu /// /// [UnityTest] - [LoadScene()] + [LoadScene] public IEnumerator TestMenuOption1() { yield return new WaitForSeconds(TimeUntilMenuIsSetup); @@ -68,7 +67,7 @@ public IEnumerator TestMenuOption1() /// /// [UnityTest] - [LoadScene()] + [LoadScene] public IEnumerator TestMenuNestedOptionOne() { yield return new WaitForSeconds(TimeUntilMenuIsSetup); @@ -85,7 +84,7 @@ public IEnumerator TestMenuNestedOptionOne() /// /// [UnityTest] - [LoadScene()] + [LoadScene] public IEnumerator TestMenuNestedOptionTwo() { yield return new WaitForSeconds(TimeUntilMenuIsSetup); @@ -102,7 +101,7 @@ public IEnumerator TestMenuNestedOptionTwo() /// /// [UnityTest] - [LoadScene()] + [LoadScene] public IEnumerator TestMenuNoOption() { yield return new WaitForSeconds(TimeUntilMenuIsSetup); @@ -131,34 +130,27 @@ protected override void CreateMenu(out GameObject menuGO, out SimpleListMenu menuEntries = new List { - new MenuEntry(selectAction: new UnityAction(() => { selection = OptionOneValue; }), - unselectAction: null, - title: OptionOne, - description: "Select option 1", - entryColor: Color.red, - enabled: true, - icon: GetIcon()), - new NestedMenuEntry(innerEntries: new List() - { - new MenuEntry(selectAction: new UnityAction(() => { selection = NestedOptionOneValue; }), - unselectAction: null, - title: NestedOptionOne, - description: "Select option 2a", - entryColor: Color.green, - enabled: true, - icon: GetIcon()), - new MenuEntry(selectAction: new UnityAction(() => { selection = NestedOptionTwoValue; }), - unselectAction: null, - title: NestedOptionTwo, - description: "Select option 2b", - entryColor: Color.green, - enabled: true, - icon: GetIcon()) + new(SelectAction: () => { selection = OptionOneValue; }, + Title: OptionOne, + Description: "Select option 1", + EntryColor: Color.red, + Icon: GetIcon()), + new NestedMenuEntry(innerEntries: new List + { + new(SelectAction: () => selection = NestedOptionOneValue, + Title: NestedOptionOne, + Description: "Select option 2a", + EntryColor: Color.green, + Icon: GetIcon()), + new(SelectAction: () => selection = NestedOptionTwoValue, + Title: NestedOptionTwo, + Description: "Select option 2b", + EntryColor: Color.green, + Icon: GetIcon()) }, title: SubMenuTitle, description: "open subselection 2", entryColor: Color.red, - enabled: true, icon: GetIcon()) }; diff --git a/Assets/SEEPlayModeTests/TestSimpleMenu.cs b/Assets/SEEPlayModeTests/TestSimpleMenu.cs index bfcdd60d6f..a630925f31 100644 --- a/Assets/SEEPlayModeTests/TestSimpleMenu.cs +++ b/Assets/SEEPlayModeTests/TestSimpleMenu.cs @@ -1,8 +1,7 @@ -using NUnit.Framework; -using System.Collections; +using System.Collections; using System.Collections.Generic; +using NUnit.Framework; using UnityEngine; -using UnityEngine.Events; using UnityEngine.TestTools; namespace SEE.UI.Menu @@ -16,10 +15,12 @@ internal class TestSimpleMenu : TestMenu /// Title of option 1 in the menu. /// private const string OptionOne = "Option 1"; + /// /// Title of option 2 in the menu. /// private const string OptionTwo = "Option 2"; + /// /// Title of the menu. /// @@ -29,7 +30,7 @@ internal class TestSimpleMenu : TestMenu /// /// [UnityTest] - [LoadScene()] + [LoadScene] public IEnumerator TestMenuOption1() { yield return new WaitForSeconds(TimeUntilMenuIsSetup); @@ -44,7 +45,7 @@ public IEnumerator TestMenuOption1() /// /// [UnityTest] - [LoadScene()] + [LoadScene] public IEnumerator TestMenuOption2() { yield return new WaitForSeconds(TimeUntilMenuIsSetup); @@ -59,7 +60,7 @@ public IEnumerator TestMenuOption2() /// /// [UnityTest] - [LoadScene()] + [LoadScene] public IEnumerator TestMenuNoOption() { yield return new WaitForSeconds(TimeUntilMenuIsSetup); @@ -87,20 +88,16 @@ protected override void CreateMenu(out GameObject menuGO, out SimpleListMenu menuEntries = new List { - new MenuEntry(selectAction: new UnityAction(() => { selection = 1; }), - unselectAction: null, - title: OptionOne, - description: "Select option 1", - entryColor: Color.red, - enabled: true, - icon: GetIcon()), - new MenuEntry(selectAction: new UnityAction(() => { selection = 2; }), - unselectAction: null, - title: OptionTwo, - description: "Select option 2", - entryColor: Color.green, - enabled: true, - icon: GetIcon()), + new(SelectAction: () => selection = 1, + Title: OptionOne, + Description: "Select option 1", + EntryColor: Color.red, + Icon: GetIcon()), + new(SelectAction: () => selection = 2, + Title: OptionTwo, + Description: "Select option 2", + EntryColor: Color.green, + Icon: GetIcon()), }; menu.AddEntries(menuEntries); diff --git a/Assets/SEETests/UI/TestMenuEntry.cs b/Assets/SEETests/UI/TestMenuEntry.cs index 8f1f1440ed..51ac42b389 100644 --- a/Assets/SEETests/UI/TestMenuEntry.cs +++ b/Assets/SEETests/UI/TestMenuEntry.cs @@ -38,10 +38,10 @@ protected static IEnumerable ValidConstructorSupplier() /// Creates a new MenuEntry, calling the constructor with the given parameters. /// /// The newly constructed MenuEntry. - protected virtual MenuEntry CreateMenuEntry(UnityAction action, string title, string description = null, + protected virtual MenuEntry CreateMenuEntry(Action action, string title, string description = null, Color entryColor = default, bool enabled = true, Sprite icon = null) { - return new MenuEntry(action, null, title, description, entryColor, enabled, icon); + return new MenuEntry(action, title, null, description, entryColor, enabled, icon); } [Test] @@ -54,7 +54,7 @@ public void TestConstructorTitleNull() [Test] public void TestConstructorDefault() { - List testItems = new List(); + List testItems = new(); void Action() => testItems.Add(1); MenuEntry entry = CreateMenuEntry(Action, "Test"); Assert.AreEqual(null, entry.Description); @@ -72,7 +72,7 @@ public void TestConstructorDefault() } [Test, TestCaseSource(nameof(ValidConstructorSupplier))] - public void TestConstructor(UnityAction action, string title, string description, + public void TestConstructor(Action action, string title, string description, Color entryColor, bool enabled, Sprite icon) { MenuEntry entry = CreateMenuEntry(action, title, description, entryColor, enabled, icon); diff --git a/Assets/SEETests/UI/TestToggleMenuEntry.cs b/Assets/SEETests/UI/TestToggleMenuEntry.cs index 4171d5679e..660145bada 100644 --- a/Assets/SEETests/UI/TestToggleMenuEntry.cs +++ b/Assets/SEETests/UI/TestToggleMenuEntry.cs @@ -1,8 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using NUnit.Framework; using SEE.Utils; using UnityEngine; -using UnityEngine.Events; namespace SEE.UI.Menu { @@ -13,18 +13,18 @@ namespace SEE.UI.Menu [TestFixture] internal class TestToggleMenuEntry: TestMenuEntry { - protected override MenuEntry CreateMenuEntry(UnityAction action, string title, string description = null, + protected override MenuEntry CreateMenuEntry(Action action, string title, string description = null, Color entryColor = default, bool enabled = true, Sprite icon = null) { - return new MenuEntry(action, null, title, description, entryColor, enabled, icon); + return new MenuEntry(action, title, null, description, entryColor, enabled, icon); } [Test] public void TestDefaultExitAction() { - MenuEntry entry1 = new( () => {}, null, "Test"); - MenuEntry entry2 = new( () => {}, null, "Test"); + MenuEntry entry1 = new(() => {}, "Test"); + MenuEntry entry2 = new(() => {}, "Test"); Assert.DoesNotThrow(() => entry1.SelectAction()); Assert.DoesNotThrow(() => entry2.SelectAction()); } @@ -38,7 +38,7 @@ public void TestExitAction() GameObject go = new("Test"); SelectionMenu selectionMenu = go.AddComponent(); void ExitAction() => testItems.Add(true); - MenuEntry entry = new(() => {}, ExitAction, "Test"); + MenuEntry entry = new(() => {}, "Test", ExitAction); selectionMenu.AddEntry(entry); Assert.AreNotEqual(entry, selectionMenu.ActiveEntry, "SelectionMenu.ActiveEntry isn't set correctly!"); Assert.AreEqual(0, testItems.Count, "Entry/ExitAction may not be called during initialization!"); From 30976a5aede46f4d8f1d3fd987701de4e4291706 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Wed, 24 Jul 2024 14:58:15 +0200 Subject: [PATCH 08/23] Fix list menu entries not being removed properly This bug previously occurred if the entries' titles contained any slashes, as this would induce different behavior in the Transform.Find method. --- Assets/SEE/UI/Menu/Desktop/SimpleListMenuDesktop.cs | 12 ++++++++---- Assets/SEE/UI/Menu/SimpleListMenu.cs | 9 +++++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Assets/SEE/UI/Menu/Desktop/SimpleListMenuDesktop.cs b/Assets/SEE/UI/Menu/Desktop/SimpleListMenuDesktop.cs index 97fb47f99b..03eb55b239 100644 --- a/Assets/SEE/UI/Menu/Desktop/SimpleListMenuDesktop.cs +++ b/Assets/SEE/UI/Menu/Desktop/SimpleListMenuDesktop.cs @@ -1,4 +1,5 @@ -using Michsky.UI.ModernUIPack; +using System.Linq; +using Michsky.UI.ModernUIPack; using SEE.Utils; using Sirenix.Utilities; using UnityEngine; @@ -52,7 +53,7 @@ public partial class SimpleListMenu where T : MenuEntry /// /// The menu entry. /// The game object of the entry. - public GameObject EntryGameObject(T entry) => EntryList.transform.Find(entry.Title)?.gameObject; + public GameObject EntryGameObject(T entry) => EntryList.transform.Cast().FirstOrDefault(x => x.name == entry.Title)?.gameObject; /// /// Initializes the menu. @@ -116,8 +117,11 @@ protected virtual void AddButton(T entry) // hover listeners PointerHelper pointerHelper = button.GetComponent(); - pointerHelper.EnterEvent.AddListener(_ => Tooltip.ActivateWith(entry.Description)); - pointerHelper.ExitEvent.AddListener(_ => Tooltip.Deactivate()); + if (entry.Description != null) + { + pointerHelper.EnterEvent.AddListener(_ => Tooltip.ActivateWith(entry.Description)); + pointerHelper.ExitEvent.AddListener(_ => Tooltip.Deactivate()); + } // adds clickEvent listener or show that button is disabled if (entry.Enabled) diff --git a/Assets/SEE/UI/Menu/SimpleListMenu.cs b/Assets/SEE/UI/Menu/SimpleListMenu.cs index 9d47491717..5a0122caa4 100644 --- a/Assets/SEE/UI/Menu/SimpleListMenu.cs +++ b/Assets/SEE/UI/Menu/SimpleListMenu.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; -using UnityEngine.Events; using UnityEngine.Windows.Speech; namespace SEE.UI.Menu @@ -98,6 +98,11 @@ public void RemoveEntry(T entry) OnEntryRemoved?.Invoke(entry); } + /// + /// Removes all menu entries. + /// + public void ClearEntries() => entries.ToList().ForEach(RemoveEntry); + /// /// Selects a menu entry. /// It is assumed that the menu contains the entry. From c56d39c554b5e4f6c40a9fc17ef39d358d9687f8 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Wed, 24 Jul 2024 15:00:03 +0200 Subject: [PATCH 09/23] Merge successive empty lines when converting from Markdown #728 --- .../SEE/Utils/Markdown/MarkdownConverter.cs | 29 +++++++++++++++++-- Assets/SEE/Utils/StringExtensions.cs | 2 +- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/Assets/SEE/Utils/Markdown/MarkdownConverter.cs b/Assets/SEE/Utils/Markdown/MarkdownConverter.cs index f120fcf0a6..db4744eef3 100644 --- a/Assets/SEE/Utils/Markdown/MarkdownConverter.cs +++ b/Assets/SEE/Utils/Markdown/MarkdownConverter.cs @@ -1,5 +1,7 @@ +using System.Collections.Generic; using System.IO; using System.Linq; +using MoreLinq.Extensions; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using UnityEngine; @@ -47,7 +49,30 @@ public static string ToRichText(this MarkedStringsOrMarkupContent content) })); } - return MarkupTextToRichText(markdown); + string richText = MarkupTextToRichText(markdown); + // We concatenate empty successive lines, which may sometimes appear in the converted rich text. + // To check if a line is empty, we need to get rid of its tags first. + return string.Join('\n', richText.Split('\n') + // We start a new segment whenever the line does not only consist of + // white space. + .Segment(x => !string.IsNullOrWhiteSpace(x.WithoutRichTextTags())) + // Then, we join the segments with a single line break. + // This way, we make sure not to accidentally remove rich text tags. + .Select(HandleSegment)); + + string HandleSegment(IEnumerable segment) + { + IList lines = segment.ToList(); + if (lines.Count == 1) + { + return lines[0]; + } + else + { + // First line should be separated by a line break so that at least one line break is present. + return lines[0] + '\n' + string.Join(string.Empty, lines.Skip(1)); + } + } } /// @@ -62,4 +87,4 @@ public static string MarkupTextToRichText(string markdownText) return writer.ToString(); } } -} \ No newline at end of file +} diff --git a/Assets/SEE/Utils/StringExtensions.cs b/Assets/SEE/Utils/StringExtensions.cs index bbed795e69..1b2c1fd336 100644 --- a/Assets/SEE/Utils/StringExtensions.cs +++ b/Assets/SEE/Utils/StringExtensions.cs @@ -63,7 +63,7 @@ public static string WrapLines(this string input, int wrapAt) /// /// The string to clean. /// The string without any rich text tags. - public static string CleanRichText(this string input) + public static string WithoutRichTextTags(this string input) { StringBuilder builder = new(); string[] segments = Regex.Split(input, "()"); From 577fd95f2fef93abbf627647fa0d736851401972 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Wed, 24 Jul 2024 15:12:31 +0200 Subject: [PATCH 10/23] Implement `ShowCodeForPath` static method This displays a code window for a given path and range, instead of for a given graph element. It works by calling the `FittingElements` extension method on the graph, also implemented in this commit. This, in turn, finds graph elements matching the given path and range, ordered by how well they fit. --- Assets/SEE/Controls/Actions/ShowCodeAction.cs | 65 +++++++++++------ Assets/SEE/DataModel/DG/GraphExtensions.cs | 69 ++++++++++++++++--- 2 files changed, 103 insertions(+), 31 deletions(-) diff --git a/Assets/SEE/Controls/Actions/ShowCodeAction.cs b/Assets/SEE/Controls/Actions/ShowCodeAction.cs index 3382aad76b..b79db6d32b 100644 --- a/Assets/SEE/Controls/Actions/ShowCodeAction.cs +++ b/Assets/SEE/Controls/Actions/ShowCodeAction.cs @@ -14,6 +14,8 @@ using SEE.Utils.History; using SEE.Game.City; using SEE.VCS; +using GraphElementRef = SEE.GO.GraphElementRef; +using Range = SEE.DataModel.DG.Range; namespace SEE.Controls.Actions { @@ -214,7 +216,7 @@ public static CodeWindow ShowVCSDiff(GraphElementRef graphElementRef, DiffCity c { case Change.Unmodified or Change.Added or Change.TypeChanged or Change.Copied or Change.Unknown: // We can show the plain file in the newer revision. - codeWindow.EnterFromText(vcs.Show(relativePath, city.NewRevision).Split(new string[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)); + codeWindow.EnterFromText(vcs.Show(relativePath, city.NewRevision).Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)); break; case Change.Modified or Change.Deleted or Change.Renamed: // If a file was renamed, it can still have differences. @@ -226,24 +228,47 @@ public static CodeWindow ShowVCSDiff(GraphElementRef graphElementRef, DiffCity c throw new Exception($"Unexpected change type {change} for {relativePath}"); } - switch (change) + codeWindow.Title = change switch { - case Change.Renamed: - codeWindow.Title = $"{oldRelativePath}" - + $" -> {sourceFilename}"; - break; - case Change.Deleted: - codeWindow.Title = $"{sourceFilename}"; - break; - default: - codeWindow.Title = sourceFilename; - break; - } + Change.Renamed => $"{oldRelativePath}" + + $" -> {sourceFilename}", + Change.Deleted => $"{sourceFilename}", + _ => sourceFilename + }; codeWindow.ScrolledVisibleLine = 1; return codeWindow; } + /// + /// Returns a CodeWindow showing the code range of the graph element most closely matching + /// the given and in the given . + /// Will return null and show an error message if no suitable graph element is found. + /// + /// The graph to search in + /// The path to search for + /// The range to search for + /// Action to be executed after the CodeWindow has been filled + /// with its content + /// new CodeWindow showing the code range of the graph element most closely matching + /// the given and + public static CodeWindow ShowCodeForPath(Graph graph, string path, Range range = null, Action ContentTextEntered = null) + { + // If we just have a path as input, we need to find a fitting graph element. + GraphElementRef element = graph.FittingElements(path, range).WithGameObject() + .Select(x => x.GameObject().MustGetComponent()) + .FirstOrDefault(); + + if (element == null) + { + ShowNotification.Error("No graph element found", + $"No suitable graph element found for path {path}", log: false); + return null; + } + + return ShowCode(element, ContentTextEntered); + } + /// /// Returns a CodeWindow showing the code range of the given graph element /// retrieved from a file. The path of the file is retrieved from @@ -251,15 +276,17 @@ public static CodeWindow ShowVCSDiff(GraphElementRef graphElementRef, DiffCity c /// attributes. /// /// The graph element to get the CodeWindow for + /// Action to be executed after the CodeWindow has been filled + /// with its content /// new CodeWindow showing the code range of the given graph element - public static CodeWindow ShowCode(GraphElementRef graphElementRef) + public static CodeWindow ShowCode(GraphElementRef graphElementRef, Action ContentTextEntered = null) { GraphElement graphElement = graphElementRef.Elem; CodeWindow codeWindow = GetOrCreateCodeWindow(graphElementRef, graphElement.Filename); - EnterWindowContent().Forget(); // This can happen in the background. + EnterWindowContent().ContinueWith(() => ContentTextEntered?.Invoke(codeWindow)); return codeWindow; - async UniTaskVoid EnterWindowContent() + async UniTask EnterWindowContent() { // We have to differentiate between a file-based and a VCS-based code city. if (graphElement.TryGetCommitID(out string commitID)) @@ -271,11 +298,10 @@ async UniTaskVoid EnterWindowContent() throw new InvalidOperationException(message); } IVersionControl vcs = VersionControlFactory.GetVersionControl(VCSKind.Git, repositoryPath); - string[] fileContent = vcs.Show(graphElement.ID, commitID). - Split("\\n", StringSplitOptions.RemoveEmptyEntries); + string[] fileContent = vcs.Show(graphElement.ID, commitID).Split("\\n", StringSplitOptions.RemoveEmptyEntries); codeWindow.EnterFromText(fileContent); } - else + else if (!codeWindow.ContainsText) { await codeWindow.EnterFromFileAsync(GetPath(graphElement).absolutePlatformPath); } @@ -286,7 +312,6 @@ async UniTaskVoid EnterWindowContent() codeWindow.ScrolledVisibleLine = line; } } - } public override bool Update() diff --git a/Assets/SEE/DataModel/DG/GraphExtensions.cs b/Assets/SEE/DataModel/DG/GraphExtensions.cs index 35d1f41005..0a8eba3a2c 100644 --- a/Assets/SEE/DataModel/DG/GraphExtensions.cs +++ b/Assets/SEE/DataModel/DG/GraphExtensions.cs @@ -29,17 +29,17 @@ public static class GraphExtensions /// the elements in both graphs that have no differences according /// to ; it belongs to public static void Diff - (this Graph newGraph, - Graph oldGraph, - Func> getElements, - Func getElement, - IGraphElementDiff diff, - GraphElementEqualityComparer comparer, - out ISet added, - out ISet removed, - out ISet changed, - out ISet equal) - where T : GraphElement + (this Graph newGraph, + Graph oldGraph, + Func> getElements, + Func getElement, + IGraphElementDiff diff, + GraphElementEqualityComparer comparer, + out ISet added, + out ISet removed, + out ISet changed, + out ISet equal) + where T : GraphElement { IEnumerable oldElements = oldGraph != null ? getElements(oldGraph).ToList() : null; IEnumerable newElements = newGraph != null ? getElements(newGraph).ToList() : null; @@ -142,5 +142,52 @@ public static IEnumerable ApplyAll(this IEnumerable modifi { return modifiers.Aggregate(elements, (current, modifier) => modifier.Apply(current)); } + + /// + /// Returns the graph elements that most closely match the given + /// and , ordered by descending specificity. + /// + /// If the is not given, we prefer file nodes + /// (as they most closely represent the file at the given path), + /// + /// If the is given, we prefer elements that have a source range + /// which contains the given range. If multiple elements have a source range that contains + /// the given range, we prefer the one with the fewest lines. + /// + /// The graph to search in + /// The path to search for + /// The range to search for + /// The graph elements that most closely match the given path and range + public static IOrderedEnumerable FittingElements(this Graph graph, string path, Range range = null) + { + return graph.Elements().Where(e => e.Path() == path).OrderBy(OrderKey); + + int OrderKey(GraphElement graphElement) + { + if (range != null && graphElement.SourceRange != null && graphElement.SourceRange.Contains(range)) + { + // The fewer lines there are (i.e., the more specific the element is), the higher this is ordered. + // The 1_000_000 is just an arbitrary large number to make sure that the range is always preferred. + return graphElement.SourceRange.Lines - 1_000_000; + } + else + { + // We prefer file nodes (as they most closely represent the file at the given path), + // but fall back to using more specific node kinds, as long as they have the same path. + if (graphElement.Type == "File") + { + return 1; + } + else if (graphElement.SourceLine == null) + { + return 2; + } + else + { + return 3; + } + } + } + } } } From 81c8b6bc6fcf485350713c64d86bbf7b23892a39 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Wed, 24 Jul 2024 15:16:43 +0200 Subject: [PATCH 11/23] Fix line marking in code windows To make things easier and less computationally intensive, we now simply make the line number of the marked line red instead of marking the whole line with some background color. --- Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs | 27 +++++++++---------- .../UI/Window/CodeWindow/CodeWindowInput.cs | 17 +++++++++--- .../UI/Window/CodeWindow/DesktopCodeWindow.cs | 4 +-- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs b/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs index 2ffdb1f66c..a1d64671bc 100644 --- a/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs @@ -97,26 +97,28 @@ public partial class CodeWindow : BaseWindow private static TMP_WordInfo? lastHoveredWord; /// - /// Visually marks the line at the given and scrolls to it. - /// Will also unmark any other line. Sets to - /// . + /// Visually highlights the line number at the given and scrolls to it. + /// Will also unhighlight any other line. Sets to . /// Clears the markers for line numbers smaller than 1. /// - /// The line number of the line to mark and scroll to (1-indexed) + /// The line number of the line to highlight and scroll to (1-indexed) public void MarkLine(int lineNumber) { + const string markColor = ""; + int markColorLength = markColor.Length; markedLine = lineNumber; - string[] allLines = textMesh.text.Split('\n').Select(x => x.EndsWith("") ? x.Substring(16, x.Length - 16 - 7) : x).ToArray(); + string[] allLines = textMesh.text.Split('\n') + .Select(x => x.StartsWith(markColor) ? $"{x[markColorLength..]}" : x) + .ToArray(); if (lineNumber < 1) { textMesh.text = string.Join("\n", allLines); } else { - string markLine = $"{allLines[lineNumber - 1]}"; - textMesh.text = string.Join("\n", allLines.Take(lineNumber - 1).Append(markLine).Concat(allLines.Skip(lineNumber).Take(lines - lineNumber + 1))); + string markLine = $"{markColor}{allLines[lineNumber - 1][markColorLength..]}"; + textMesh.text = string.Join("\n", allLines.Exclude(lineNumber - 1, 1).Insert(new[] { markLine }, lineNumber - 1)); } - } #region Visible Line Calculation @@ -175,12 +177,7 @@ public int ScrolledVisibleLine DOTween.Sequence().Append(DOTween.To(() => ImmediateVisibleLine, f => ImmediateVisibleLine = f, value - 1, 1f)) .AppendCallback(() => scrollingTo = 0); - // FIXME (#250): TMP bug: Large files cause issues with highlighting text. This is just a workaround. - // See https://github.com/uni-bremen-agst/SEE/issues/250#issuecomment-819653373 - if (text.Length < 16382) - { - MarkLine(value); - } + MarkLine(value); ScrollEvent.Invoke(); } @@ -210,7 +207,7 @@ private float ImmediateVisibleLine } else { - scrollRect.verticalNormalizedPosition = 1 - value / (lines - 1 - excessLines); + scrollRect.verticalNormalizedPosition = 1 - (value-1) / (lines - 1 - excessLines); scrollRect.horizontalNormalizedPosition = 0; } } diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs b/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs index aff32a9c4b..eba88bd439 100644 --- a/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using Cysharp.Threading.Tasks; +using SEE.DataModel.DG; using SEE.Game; using SEE.Game.City; using SEE.GO; @@ -53,6 +54,11 @@ public partial class CodeWindow /// private List CodeWindowOffsets { get; } = new(); + /// + /// The graph associated with the code city this code window is in. + /// + private Graph AssociatedGraph; + /// /// Characters representing newlines. /// Note that newlines may also consist of aggregations of this set (e.g. "\r\n"). @@ -90,7 +96,7 @@ private void EnterFromTokens(IEnumerable tokens, + tokenList.Where(x => !x.TokenType.Equals(TokenType.Newline)) .Aggregate(0, (_, token) => token.Text.Count(x => x == '\n')); // Needed padding is the number of lines, because the line number will be at most this long. - neededPadding = assumedLines.ToString().Length; + neededPadding = Mathf.FloorToInt(Mathf.Log10(assumedLines)) + 1; text = $"{string.Join("", Enumerable.Repeat(" ", neededPadding - 1))}1 "; CodeWindowOffsets.Clear(); @@ -141,9 +147,9 @@ void AppendNewline(ref int theLineNumber, ref string text, SEEToken token) // + 1 for the newline CodeWindowOffsets.Add(++characterOffset); // Add whitespace next to line number, so it's consistent. - text += string.Join("", Enumerable.Repeat(' ', neededPadding - $"{theLineNumber}".Length)); + int padding = neededPadding - (Mathf.FloorToInt(Mathf.Log10(theLineNumber)) + 1); // Line number will be typeset in grey to distinguish it from the rest. - text += $"{theLineNumber} "; + text += $"{string.Join(string.Empty, Enumerable.Repeat(' ', padding))}{theLineNumber} "; characterOffset += neededPadding + 1; if (issues?.ContainsKey(theLineNumber) ?? false) @@ -347,7 +353,7 @@ public void EnterFromText(string[] text, bool asIs = false) // Add whitespace next to line number so it's consistent. this.text += string.Join("", Enumerable.Repeat(" ", neededPadding - $"{i + 1}".Length)); // Line number will be typeset in yellow to distinguish it from the rest. - this.text += $"{i + 1} "; + this.text += $"{i + 1} "; if (asIs) { this.text += text[i] + "\n"; @@ -423,6 +429,7 @@ public async UniTask EnterFromFileAsync(string filename) if (go.TryGetComponentOrLog(out AbstractSEECity city)) { + AssociatedGraph = city.LoadedGraph; bool useDashboardIssues = city.ErosionSettings.ShowDashboardIssuesInCodeWindow; bool useLspIssues = lspHandler != null && lspHandler.UseInCodeWindows; MarkIssuesAsync(filename, useDashboardIssues, useLspIssues).Forget(); // initiate issue search in background @@ -499,6 +506,8 @@ private async UniTaskVoid MarkIssuesAsync(string path, bool useDashboardIssues, { textMesh.text = text; textMesh.ForceMeshUpdate(); + // Will need to be marked again after the text has been updated. + MarkLine(ScrolledVisibleLine); SetupBreakpoints(); } catch (IndexOutOfRangeException) diff --git a/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs b/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs index 5b6e7a1ad2..48acd13024 100644 --- a/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs +++ b/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs @@ -66,8 +66,8 @@ protected override void StartDesktop() DebugBreakpointManager.OnBreakpointAdded += OnBreakpointAdded; DebugBreakpointManager.OnBreakpointRemoved += OnBreakpointRemoved; - Transform temp = SceneQueries.GetCodeCity(transform); - if (temp && temp.gameObject.TryGetComponentOrLog(out AbstractSEECity city)) + Transform cityTransform = SceneQueries.GetCodeCity(transform); + if (cityTransform && cityTransform.gameObject.TryGetComponentOrLog(out AbstractSEECity city)) { // Get button for IDE interaction and register events. Window.transform.Find("Dragger/IDEButton").gameObject.GetComponent public TimeSpan TimeoutSpan = TimeSpan.FromSeconds(2); + /// + /// The URI of the project. + /// + public Uri ProjectUri => new(ProjectPath, UriKind.Absolute); + /// /// The capabilities of the language server. /// diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs b/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs index a1d64671bc..6d3d461c84 100644 --- a/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs @@ -2,6 +2,7 @@ using System.Linq; using Cysharp.Threading.Tasks; using DG.Tweening; +using MoreLinq; using SEE.Tools.LSP; using SEE.Utils; using TMPro; @@ -96,6 +97,11 @@ public partial class CodeWindow : BaseWindow /// private static TMP_WordInfo? lastHoveredWord; + /// + /// Whether the code window contains text. + /// + public bool ContainsText => text != null; + /// /// Visually highlights the line number at the given and scrolls to it. /// Will also unhighlight any other line. Sets to . diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs b/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs new file mode 100644 index 0000000000..79a233dc4a --- /dev/null +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Cysharp.Threading.Tasks; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using SEE.Controls; +using SEE.Controls.Actions; +using SEE.UI.Menu; +using SEE.UI.Notification; +using SEE.UI.PopupMenu; +using SEE.Utils; +using UnityEngine; +using UnityEngine.Assertions; +using Range = SEE.DataModel.DG.Range; + +namespace SEE.UI.Window.CodeWindow +{ + public partial class CodeWindow + { + /// + /// The context menu that this class manages. + /// + private PopupMenu.PopupMenu contextMenu; + + /// + /// A list menu that's shown when the user needs to make some selection, + /// such as when choosing a reference to navigate to. + /// + private SimpleListMenu simpleListMenu; + + /// + /// Shows the context menu at the given , assuming the user right-clicked + /// at the given and . + /// + /// The 0-indexed line where the user right-clicked. + /// The 0-indexed column where the user right-clicked. + /// The position where the context menu should be shown. + /// The word at the given and . + private void ShowContextMenu(int line, int column, Vector2 position, string contextWord) + { + IList actions = new List(); + + // TODO: Type Hierarchy and Call Hierarchy + if (lspHandler.ServerCapabilities.ReferencesProvider != null) + { + actions.Add(new("Show References", ShowReferences, Icons.IncomingEdge)); + } + if (lspHandler.ServerCapabilities.DeclarationProvider != null) + { + actions.Add(new("Show Declaration", ShowDeclaration, Icons.OutgoingEdge)); + } + if (lspHandler.ServerCapabilities.DefinitionProvider != null) + { + actions.Add(new("Show Definition", ShowDefinition, Icons.OutgoingEdge)); + } + if (lspHandler.ServerCapabilities.ImplementationProvider != null) + { + actions.Add(new("Show Implementation", ShowImplementation, Icons.OutgoingEdge)); + } + if (lspHandler.ServerCapabilities.TypeDefinitionProvider != null) + { + actions.Add(new("Show Type Definition", ShowTypeDefinition, Icons.OutgoingEdge)); + } + + if (actions.Count == 0) + { + return; + } + contextMenu.ShowWith(actions, position); + + return; + + void ShowReferences() => + ShowLocationsAsync(lspHandler.References(FilePath, line, column, includeDeclaration: true), + "References").Forget(); + + void ShowDeclaration() => + ShowLocationsAsync(lspHandler.Declaration(FilePath, line, column), "Declarations").Forget(); + + void ShowDefinition() => + ShowLocationsAsync(lspHandler.Definition(FilePath, line, column), "Definitions").Forget(); + + void ShowImplementation() => + ShowLocationsAsync(lspHandler.Implementation(FilePath, line, column), "Implementations").Forget(); + + void ShowTypeDefinition() => + ShowLocationsAsync(lspHandler.TypeDefinition(FilePath, line, column), "Type Definitions").Forget(); + + // Opens a menu with the given locations and name. Clicking on an entry will open the location. + async UniTaskVoid ShowLocationsAsync(IUniTaskAsyncEnumerable locations, + string name) + { + using (LoadingSpinner.ShowIndeterminate($"Loading {name} for \"{contextWord}\"...")) + { + IList entries = new List(); + await foreach (LocationOrLocationLink location in locations) + { + Range targetRange; + Uri targetUri; + if (location.IsLocation) + { + Location loc = location.Location!; + targetRange = Range.FromLspRange(loc.Range); + targetUri = loc.Uri.ToUri(); + } + else + { + LocationLink locLink = location.LocationLink!; + targetRange = Range.FromLspRange(locLink.TargetRange); + targetUri = locLink.TargetUri.ToUri(); + } + Uri path = targetUri; + if (lspHandler.ProjectUri?.IsBaseOf(path) ?? false) + { + // Truncate path above the project's base path to make the result more readable. + path = lspHandler.ProjectUri.MakeRelativeUri(path); + } + entries.Add(new(SelectAction: () => OpenSelection(targetUri, targetRange), + Title: $"{path}: {targetRange}", + // TODO: Icon + EntryColor: new Color(0.051f, 0.3608f, 0.1333f))); + } + await UniTask.SwitchToMainThread(); + switch (entries.Count) + { + case 0: + ShowNotification.Info("No results", $"No {name} found for \"{contextWord}\".", log: false); + break; + case 1: + // We can directly open the only result. + entries.First().SelectAction(); + break; + default: + // The user needs to select one of the results. + simpleListMenu.ClearEntries(); + simpleListMenu.AddEntries(entries); + simpleListMenu.Title = name; + simpleListMenu.Description = $"Listing {name} for {contextWord}."; + simpleListMenu.Icon = Resources.Load("Materials/Notification/info"); + simpleListMenu.ShowMenu = true; + break; + } + } + } + + // Opens the selection at the given uri and range. If the uri is the current file, we just + // scroll to the range, otherwise we open a new (or existing) code window. + void OpenSelection(Uri uri, Range range) + { + // If this is the current file, we can just scroll to the range. + if (FilePath == uri.LocalPath) + { + // TODO: Allow going back (button or keyboard shortcut) + ScrolledVisibleLine = range.Start.Line; + } + else + { + // Otherwise, we need to open a different code window. + Assert.IsNotNull(AssociatedGraph); + CodeWindow window = ShowCodeAction.ShowCodeForPath(AssociatedGraph, uri.LocalPath, range, + w => w.ScrolledVisibleLine = range.Start.Line); + if (window != null) + { + WindowSpace manager = WindowSpaceManager.ManagerInstance[WindowSpaceManager.LocalPlayer]; + if (!manager.Windows.Contains(window)) + { + manager.AddWindow(window); + } + manager.ActiveWindow = window; + } + } + } + } + } +} diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs.meta b/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs.meta new file mode 100644 index 0000000000..c1958dc55e --- /dev/null +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 705487168bd5437592d4f98269826340 +timeCreated: 1721735361 \ No newline at end of file diff --git a/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs b/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs index 48acd13024..d846c8d1b3 100644 --- a/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs +++ b/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs @@ -16,6 +16,7 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Models; using SEE.Controls; using SEE.UI.DebugAdapterProtocol; +using SEE.UI.Menu; using SEE.Utils.Markdown; using UnityEngine.Assertions; @@ -54,6 +55,13 @@ protected override void StartDesktop() scrollable = PrefabInstantiator.InstantiatePrefab(codeWindowPrefab, Window.transform.Find("Content"), false); scrollable.name = "Scrollable"; + // Initialize list and context menu, if necessary. + if (lspHandler != null) + { + contextMenu = gameObject.AddComponent(); + simpleListMenu = gameObject.AddComponent(); + } + // Set text and preferred font size GameObject code = scrollable.transform.Find("Code").gameObject; if (code.TryGetComponentOrLog(out textMesh)) @@ -162,24 +170,36 @@ protected override void UpdateDesktop() { if (WindowSpaceManager.ManagerInstance[WindowSpaceManager.LocalPlayer].ActiveWindow == this) { - // Show issue info on click (on hover would be too expensive) + // Right-click opens menu with LSP actions. + if (Input.GetMouseButtonDown(1)) + { + if (lspHandler != null) + { + // We use the word detection instead of the character detection because the latter + // needs the cursor to be precisely over a character, while the former works more broadly. + if (DetectHoveredWord() is { } word) + { + int character = word.firstCharacterIndex; + (int line, int column) = GetSourcePosition(character); + ShowContextMenu(line-1, column-1, Input.mousePosition, word.GetWord()); + } + return; + } + } if (issueDictionary.Count != 0) { - // Passing camera as null causes the screen space rather than world space camera to be used + // Show issue info by leveraging links we created earlier. + // Passing camera as null causes the screen space rather than world space camera to be used. int link = TMP_TextUtilities.FindIntersectingLink(textMesh, Input.mousePosition, null); if (link != -1) { - char linkId = textMesh.textInfo.linkInfo[link].GetLinkID()[0]; - // Display tooltip containing all issue descriptions - UniTask.WhenAll(issueDictionary[linkId].Select(x => x.ToCodeWindowStringAsync())) - .ContinueWith(x => Tooltip.ActivateWith(string.Join('\n', x), Tooltip.AfterShownBehavior.HideUntilActivated)) - .Forget(); + TriggerIssueHoverAsync(link).Forget(); + return; } } + TMP_WordInfo? hoveredWord = DetectHoveredWord(); // Detect hovering over words - int index = TMP_TextUtilities.FindIntersectingWord(textMesh, Input.mousePosition, null); - TMP_WordInfo? hoveredWord = index >= 0 && index < textMesh.textInfo.wordCount ? textMesh.textInfo.wordInfo[index] : null; if (!lastHoveredWord.Equals(hoveredWord)) { if (lspHandler != null) @@ -198,6 +218,24 @@ protected override void UpdateDesktop() } lastHoveredWord = hoveredWord; } + return; + + TMP_WordInfo? DetectHoveredWord() + { + int index = TMP_TextUtilities.FindIntersectingWord(textMesh, Input.mousePosition, null); + return index >= 0 && index < textMesh.textInfo.wordCount ? textMesh.textInfo.wordInfo[index] : null; + } + } + + /// + /// Triggers the hover event for issues. + /// + private async UniTaskVoid TriggerIssueHoverAsync(int link) + { + char linkId = textMesh.textInfo.linkInfo[link].GetLinkID()[0]; + // Display tooltip containing all issue descriptions + IEnumerable issueTexts = await UniTask.WhenAll(issueDictionary[linkId].Select(x => x.ToCodeWindowStringAsync())); + Tooltip.ActivateWith(string.Join('\n', issueTexts), Tooltip.AfterShownBehavior.HideUntilActivated); } /// @@ -222,7 +260,7 @@ private async UniTaskVoid TriggerLspHoverAsync(TMP_WordInfo? hoveredWord) Hover hoverInfo = await lspHandler.HoverAsync(FilePath, line - 1, column - 1); if (hoverInfo?.Contents != null && lastHoveredWord != null) { - Tooltip.ActivateWith(hoverInfo.Contents.ToRichText(), Tooltip.AfterShownBehavior.HideUntilActivated); + Tooltip.ActivateWith(hoverInfo.Contents.ToRichText()); } } } diff --git a/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs b/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs index 389864c406..8027ade27c 100644 --- a/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs +++ b/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs @@ -404,7 +404,7 @@ void RegisterClickHandler() } // We want all applicable actions for the element, except ones where the element - // element is shown in the TreeWindow, since we are already in the TreeWindow. + // is shown in the TreeWindow, since we are already in the TreeWindow. IEnumerable actions = ContextMenuAction .GetApplicableOptions(representedGraphElement, representedGameObject) From 54059895e5e76d1d3c20a0452117dadafc8312fd Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Sat, 27 Jul 2024 00:05:48 +0200 Subject: [PATCH 13/23] Fix the LSP call hierarchy methods #686 --- Assets/SEE/DataModel/DG/IO/LSPImporter.cs | 9 ++++----- Assets/SEE/SEE.asmdef | 3 ++- Assets/SEE/Tools/LSP/LSPHandler.cs | 20 +++++++++++++++++++- Assets/SEE/Utils/AsyncUtils.cs | 11 +++++++++++ 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/Assets/SEE/DataModel/DG/IO/LSPImporter.cs b/Assets/SEE/DataModel/DG/IO/LSPImporter.cs index 3e5e2d7576..e4ee030187 100644 --- a/Assets/SEE/DataModel/DG/IO/LSPImporter.cs +++ b/Assets/SEE/DataModel/DG/IO/LSPImporter.cs @@ -304,8 +304,7 @@ public async UniTask LoadAsync(Graph graph, Action changePercentage = nul } if (IncludeEdgeTypes.HasFlag(EdgeKind.Call) && Handler.ServerCapabilities.CallHierarchyProvider.TrueOrValue()) { - // FIXME (external: OmniSharp bug, sends wrong method name) - // await HandleCallHierarchyAsync(node, graph, token); + await HandleCallHierarchyAsync(node, graph, token); } if (IncludeEdgeTypes.HasFlag(EdgeKind.Extend) && Handler.ServerCapabilities.TypeHierarchyProvider.TrueOrValue()) { @@ -388,7 +387,7 @@ private void HandleDiagnostics(IEnumerable diagnostics, string path) /// A cancellation token that can be used to cancel the operation. private async UniTask HandleCallHierarchyAsync(Node node, Graph graph, CancellationToken token) { - IUniTaskAsyncEnumerable results = Handler.OutgoingCalls(SelectItem, node.Path(), node.SourceLine ?? 0, node.SourceColumn ?? 0); + IUniTaskAsyncEnumerable results = Handler.OutgoingCalls(SelectItem, node.Path(), node.SourceLine - 1 ?? 0, node.SourceColumn - 1 ?? 0); await foreach (CallHierarchyItem item in results) { if (token.IsCancellationRequested) @@ -407,7 +406,7 @@ private async UniTask HandleCallHierarchyAsync(Node node, Graph graph, Cancellat bool SelectItem(CallHierarchyItem item) { - return item.Uri.Path == node.Path() && node.SourceRange.Contains(Range.FromLspRange(item.Range)); + return item.Uri.Path == node.Path() && Range.FromLspRange(item.Range) == node.SourceRange; } } @@ -420,7 +419,7 @@ bool SelectItem(CallHierarchyItem item) /// A cancellation token that can be used to cancel the operation. private async UniTask HandleTypeHierarchyAsync(Node node, Graph graph, CancellationToken token) { - IUniTaskAsyncEnumerable results = Handler.Supertypes(SelectItem, node.Path(), node.SourceLine ?? 0, node.SourceColumn ?? 0); + IUniTaskAsyncEnumerable results = Handler.Supertypes(SelectItem, node.Path(), node.SourceLine - 1 ?? 0, node.SourceColumn - 1 ?? 0); await foreach (TypeHierarchyItem item in results) { if (token.IsCancellationRequested) diff --git a/Assets/SEE/SEE.asmdef b/Assets/SEE/SEE.asmdef index ff55935359..da662f513f 100644 --- a/Assets/SEE/SEE.asmdef +++ b/Assets/SEE/SEE.asmdef @@ -66,7 +66,8 @@ "Markdig.dll", "System.Collections.Immutable.dll", "MoreLinq.dll", - "Microsoft.Extensions.FileSystemGlobbing.dll" + "Microsoft.Extensions.FileSystemGlobbing.dll", + "MediatR.dll" ], "autoReferenced": false, "defineConstraints": [], diff --git a/Assets/SEE/Tools/LSP/LSPHandler.cs b/Assets/SEE/Tools/LSP/LSPHandler.cs index b8ef8f727b..797191d30c 100644 --- a/Assets/SEE/Tools/LSP/LSPHandler.cs +++ b/Assets/SEE/Tools/LSP/LSPHandler.cs @@ -418,6 +418,12 @@ private void HandleDiagnostics(PublishDiagnosticsParams diagnosticsParams) /// The parameters of the ShowMessage notification. private void ShowMessage(ShowMessageParams showMessageParams) { + if (showMessageParams.Message.Contains("window/workDoneProgress/cancel")) + { + // Cancellation messages are sometimes sent to the language server even when they don't support them. + // We can safely ignore any failing cancellations. + return; + } string languageServerName = Server?.Name ?? "Language Server"; switch (showMessageParams.Type) { @@ -678,8 +684,20 @@ public IUniTaskAsyncEnumerable OutgoingCalls(Func Client.RequestCallHierarchyOutgoing(outgoingParams, t), TimeoutSpan).Select(x => x.To); + // We can not use the built-in method here and have to make the request manually, + // as the specialized method contains a bug (issue #1303 in OmniSharp/csharp-language-server-protocol). + // return AsyncUtils.ObserveUntilTimeout(t => Client.RequestCallHierarchyOutgoing(outgoingParams, t), TimeoutSpan).Select(x => x.To); + return AsyncUtils.RunWithTimeoutAsync(MakeOutgoingCallRequest(outgoingParams), TimeoutSpan) + .AsUniTaskAsyncEnumerable() + .Select(y => y.To); }); + + Func>> MakeOutgoingCallRequest(CallHierarchyOutgoingCallsParams outgoingParams) + { + return token => Client.SendRequest("callHierarchy/outgoingCalls", outgoingParams) + .Returning>(token) + .AsUniTask(useCurrentSynchronizationContext: false); + } } /// diff --git a/Assets/SEE/Utils/AsyncUtils.cs b/Assets/SEE/Utils/AsyncUtils.cs index 1b710687e0..18c1cf392f 100644 --- a/Assets/SEE/Utils/AsyncUtils.cs +++ b/Assets/SEE/Utils/AsyncUtils.cs @@ -20,6 +20,17 @@ public static class AsyncUtils /// public static int MainThreadId = 0; + /// + /// Converts the given to an asynchronous UniTask enumerable. + /// + /// The task of enumerables to convert. + /// The type of the elements in the enumerable. + /// An asynchronous UniTask enumerable that emits the elements of the enumerable. + public static IUniTaskAsyncEnumerable AsUniTaskAsyncEnumerable(this UniTask> task) + { + return task.ToUniTaskAsyncEnumerable().SelectMany(x => x.ToUniTaskAsyncEnumerable()); + } + /// /// Runs the given with a and returns the result. /// Note that a timeout of will cause no timeout to be applied. From 57900719895a47a95977f3ac84df93339378dedf Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Sat, 27 Jul 2024 00:06:48 +0200 Subject: [PATCH 14/23] Implement support for LSP call/type hierarchy in code windows #686 --- .../CodeWindow/CodeWindowContextMenu.cs | 129 ++++++++++++------ Assets/SEE/Utils/Icons.cs | 1 + 2 files changed, 87 insertions(+), 43 deletions(-) diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs b/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs index 79a233dc4a..3d98edd746 100644 --- a/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Cysharp.Threading.Tasks; +using Cysharp.Threading.Tasks.Linq; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using SEE.Controls; using SEE.Controls.Actions; @@ -47,19 +48,27 @@ private void ShowContextMenu(int line, int column, Vector2 position, string cont } if (lspHandler.ServerCapabilities.DeclarationProvider != null) { - actions.Add(new("Show Declaration", ShowDeclaration, Icons.OutgoingEdge)); + actions.Add(new("Go to Declaration", ShowDeclaration, Icons.OutgoingEdge)); } if (lspHandler.ServerCapabilities.DefinitionProvider != null) { - actions.Add(new("Show Definition", ShowDefinition, Icons.OutgoingEdge)); + actions.Add(new("Go to Definition", ShowDefinition, Icons.OutgoingEdge)); } if (lspHandler.ServerCapabilities.ImplementationProvider != null) { - actions.Add(new("Show Implementation", ShowImplementation, Icons.OutgoingEdge)); + actions.Add(new("Go to Implementation", ShowImplementation, Icons.OutgoingEdge)); } if (lspHandler.ServerCapabilities.TypeDefinitionProvider != null) { - actions.Add(new("Show Type Definition", ShowTypeDefinition, Icons.OutgoingEdge)); + actions.Add(new("Go to Type Definition", ShowTypeDefinition, Icons.OutgoingEdge)); + } + if (lspHandler.ServerCapabilities.CallHierarchyProvider != null) + { + actions.Add(new("Show Outgoing Calls", ShowOutgoingCalls, Icons.Sitemap)); + } + if (lspHandler.ServerCapabilities.TypeHierarchyProvider != null) + { + actions.Add(new("Show Supertypes", ShowSupertypes, Icons.Sitemap)); } if (actions.Count == 0) @@ -70,6 +79,22 @@ private void ShowContextMenu(int line, int column, Vector2 position, string cont return; + #region Local Functions + + void ShowOutgoingCalls() + { + MenuEntriesForLocationsAsync(lspHandler.OutgoingCalls(_ => true, FilePath, line, column) + .Select(x => (x.Uri.ToUri(), Range.FromLspRange(x.Range), x.Name))) + .ContinueWith(entries => ShowEntries(entries, "Outgoing Calls")).Forget(); + } + + void ShowSupertypes() + { + MenuEntriesForLocationsAsync(lspHandler.Supertypes(_ => true, FilePath, line, column) + .Select(x => (x.Uri.ToUri(), Range.FromLspRange(x.Range), x.Name))) + .ContinueWith(entries => ShowEntries(entries, "Supertypes")).Forget(); + } + void ShowReferences() => ShowLocationsAsync(lspHandler.References(FilePath, line, column, includeDeclaration: true), "References").Forget(); @@ -86,29 +111,39 @@ void ShowImplementation() => void ShowTypeDefinition() => ShowLocationsAsync(lspHandler.TypeDefinition(FilePath, line, column), "Type Definitions").Forget(); - // Opens a menu with the given locations and name. Clicking on an entry will open the location. - async UniTaskVoid ShowLocationsAsync(IUniTaskAsyncEnumerable locations, - string name) + async UniTask ShowLocationsAsync(IUniTaskAsyncEnumerable locations, string name) + { + IList entries = await MenuEntriesForLocationsAsync(locations.Select(DeconstructLocation)); + ShowEntries(entries, name); + } + + (Uri, Range, string) DeconstructLocation(LocationOrLocationLink location) + { + Range targetRange; + Uri targetUri; + if (location.IsLocation) + { + Location loc = location.Location!; + targetRange = Range.FromLspRange(loc.Range); + targetUri = loc.Uri.ToUri(); + } + else + { + LocationLink locLink = location.LocationLink!; + targetRange = Range.FromLspRange(locLink.TargetRange); + targetUri = locLink.TargetUri.ToUri(); + } + return (targetUri, targetRange, null); + } + + // Generates menu entries for the given locations. Clicking on an entry will open the location. + async UniTask> MenuEntriesForLocationsAsync(IUniTaskAsyncEnumerable<(Uri, Range, string)> locations) { + IList entries = new List(); using (LoadingSpinner.ShowIndeterminate($"Loading {name} for \"{contextWord}\"...")) { - IList entries = new List(); - await foreach (LocationOrLocationLink location in locations) + await foreach ((Uri targetUri, Range targetRange, string title) in locations) { - Range targetRange; - Uri targetUri; - if (location.IsLocation) - { - Location loc = location.Location!; - targetRange = Range.FromLspRange(loc.Range); - targetUri = loc.Uri.ToUri(); - } - else - { - LocationLink locLink = location.LocationLink!; - targetRange = Range.FromLspRange(locLink.TargetRange); - targetUri = locLink.TargetUri.ToUri(); - } Uri path = targetUri; if (lspHandler.ProjectUri?.IsBaseOf(path) ?? false) { @@ -116,30 +151,36 @@ async UniTaskVoid ShowLocationsAsync(IUniTaskAsyncEnumerable OpenSelection(targetUri, targetRange), - Title: $"{path}: {targetRange}", + Title: title ?? $"{path}: {targetRange}", // TODO: Icon EntryColor: new Color(0.051f, 0.3608f, 0.1333f))); } await UniTask.SwitchToMainThread(); - switch (entries.Count) - { - case 0: - ShowNotification.Info("No results", $"No {name} found for \"{contextWord}\".", log: false); - break; - case 1: - // We can directly open the only result. - entries.First().SelectAction(); - break; - default: - // The user needs to select one of the results. - simpleListMenu.ClearEntries(); - simpleListMenu.AddEntries(entries); - simpleListMenu.Title = name; - simpleListMenu.Description = $"Listing {name} for {contextWord}."; - simpleListMenu.Icon = Resources.Load("Materials/Notification/info"); - simpleListMenu.ShowMenu = true; - break; - } + } + return entries; + } + + // Opens a menu with the given menu entries. + void ShowEntries(IList entries, string name) + { + switch (entries.Count) + { + case 0: + ShowNotification.Info("No results", $"No {name} found for \"{contextWord}\".", log: false); + break; + case 1: + // We can directly open the only result. + entries.First().SelectAction(); + break; + default: + // The user needs to select one of the results. + simpleListMenu.ClearEntries(); + simpleListMenu.AddEntries(entries); + simpleListMenu.Title = name; + simpleListMenu.Description = $"Listing {name.ToLower()} for {contextWord}."; + simpleListMenu.Icon = Resources.Load("Materials/Notification/info"); + simpleListMenu.ShowMenu = true; + break; } } @@ -170,6 +211,8 @@ void OpenSelection(Uri uri, Range range) } } } + + #endregion } } } diff --git a/Assets/SEE/Utils/Icons.cs b/Assets/SEE/Utils/Icons.cs index 7c87efb40d..705e3f0290 100644 --- a/Assets/SEE/Utils/Icons.cs +++ b/Assets/SEE/Utils/Icons.cs @@ -42,5 +42,6 @@ public static class Icons public const char CircleCheckmark = '\uF058'; public const char CircleQuestionMark = '\uF059'; public const char CircleExclamationMark = '\uF06A'; + public const char Sitemap = '\uF0E8'; } } From ff311ab4a7da4a45b9cdf5765945e9fef6bb1f4a Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Sat, 27 Jul 2024 17:41:01 +0200 Subject: [PATCH 15/23] Implement timeout for DashboardRetriever --- .../SEE/Net/Dashboard/DashboardException.cs | 4 +-- Assets/SEE/Net/Dashboard/DashboardResult.cs | 6 ++--- .../SEE/Net/Dashboard/DashboardRetriever.cs | 25 ++++++++++++++----- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/Assets/SEE/Net/Dashboard/DashboardException.cs b/Assets/SEE/Net/Dashboard/DashboardException.cs index 6adc3dd3f2..3491df3d54 100644 --- a/Assets/SEE/Net/Dashboard/DashboardException.cs +++ b/Assets/SEE/Net/Dashboard/DashboardException.cs @@ -27,7 +27,7 @@ public DashboardException(DashboardError error) : this($"{error.Type}: {error.Lo /// Instantiates a new with the given exception. /// /// Exception which occurred when accessing the dashboard API. - public DashboardException(Exception inner) : this("An error occurred while retrieving dashboard data.", inner) + public DashboardException(Exception inner) : this($"An error occurred while retrieving dashboard data ({inner.Message}).", inner) { // Intentionally empty, the other constructor that's being called is already doing all the work. } @@ -46,4 +46,4 @@ private DashboardException(string message, Exception inner) : base(message, inne #endregion } -} \ No newline at end of file +} diff --git a/Assets/SEE/Net/Dashboard/DashboardResult.cs b/Assets/SEE/Net/Dashboard/DashboardResult.cs index a41b94bf25..f0c169e388 100644 --- a/Assets/SEE/Net/Dashboard/DashboardResult.cs +++ b/Assets/SEE/Net/Dashboard/DashboardResult.cs @@ -15,7 +15,7 @@ public class DashboardResult /// /// Whether the API call has been successful. /// - public bool Success { get; private set; } + public bool Success { get; private init; } /// /// This contains the error object which has been returned from the dashboard, if the call was not successful @@ -115,7 +115,7 @@ public T RetrieveObject(bool strict = true) MissingMemberHandling = strict ? MissingMemberHandling.Error : MissingMemberHandling.Ignore }); } - catch (Exception e) when (e is JsonSerializationException || e is JsonReaderException) + catch (Exception e) when (e is JsonSerializationException or JsonReaderException) { Debug.LogError($"Error encountered: {e.Message}.\nGiven JSON was: {JSON}"); throw; @@ -131,4 +131,4 @@ public override string ToString() return $"{nameof(Error)}: {Error}, {nameof(Exception)}: {Exception}, {nameof(JSON)}: {JSON}, {nameof(Success)}: {Success}"; } } -} \ No newline at end of file +} diff --git a/Assets/SEE/Net/Dashboard/DashboardRetriever.cs b/Assets/SEE/Net/Dashboard/DashboardRetriever.cs index 0d4bb8d943..be217b90dd 100644 --- a/Assets/SEE/Net/Dashboard/DashboardRetriever.cs +++ b/Assets/SEE/Net/Dashboard/DashboardRetriever.cs @@ -76,6 +76,16 @@ public partial class DashboardRetriever : MonoBehaviour [Tooltip("Whether to throw an error if Dashboard models have more fields than the C# models.")] public bool StrictMode = false; + /// + /// The number of seconds after which a request to the dashboard will be considered timed out. + /// If this is set to 0, the request will never time out. + /// + [EnvironmentVariable("DASHBOARD_TIMEOUT_SECONDS")] + [Tooltip("The number of seconds after which a request to the dashboard will be considered timed out. " + + "If this is set to 0, the request will never time out.")] + [Min(0)] + public float TimeoutSeconds = 5; + [Header("Issue Retrieval")] /// /// Whether s shall be retrieved when calling . @@ -159,7 +169,7 @@ private async UniTask GetAtPathAsync(string path, Dictionary 0) + if (queryParameters is { Count: > 0 }) { requestUrl += "?" + Encoding.UTF8.GetString(UnityWebRequest.SerializeSimpleForm(queryParameters)); } @@ -172,10 +182,13 @@ private async UniTask GetAtPathAsync(string path, Dictionary request.isDone); + request.SendWebRequest(); + bool timeout = !await AsyncUtils.RunWithTimeoutAsync(t => UniTask.WaitUntil(() => request.isDone, cancellationToken: t), + TimeSpan.FromSeconds(TimeoutSeconds), throwOnTimeout: false); + if (timeout) + { + return new DashboardResult(new TimeoutException("Request timed out.")); + } DashboardResult result = request.result switch { UnityWebRequest.Result.Success => new DashboardResult(true, request.downloadHandler.text), @@ -264,7 +277,7 @@ private async UniTaskVoid VerifyVersionNumberAsync() ShowNotification.Error("Dashboard Version too old", $"The version of the dashboard is {version}, but the DashboardRetriever " + $"has been written for version {DashboardVersion.SupportedVersion}." - + $" Please update your dashboard."); + + " Please update your dashboard."); break; case DashboardVersion.Difference.PathOlder: // If patch version is older, there may be some bugfixes / security problems not accounted for. From f0db0584b2fca16f0223287af628e608d20cb81d Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Sat, 27 Jul 2024 17:52:55 +0200 Subject: [PATCH 16/23] Code Windows: Go to definition on ctrl+click #686 --- Assets/SEE/Controls/KeyActions/KeyBindings.cs | 3 +- Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs | 12 +- .../CodeWindow/CodeWindowContextMenu.cs | 304 ++++++++++++------ .../UI/Window/CodeWindow/CodeWindowInput.cs | 2 +- .../UI/Window/CodeWindow/DesktopCodeWindow.cs | 82 ++++- Assets/SEE/Utils/Icons.cs | 1 + 6 files changed, 285 insertions(+), 119 deletions(-) diff --git a/Assets/SEE/Controls/KeyActions/KeyBindings.cs b/Assets/SEE/Controls/KeyActions/KeyBindings.cs index 93d341f024..22975e39dd 100644 --- a/Assets/SEE/Controls/KeyActions/KeyBindings.cs +++ b/Assets/SEE/Controls/KeyActions/KeyBindings.cs @@ -12,7 +12,7 @@ internal static class KeyBindings { // IMPORTANT NOTES: // (1) Keep in mind that KeyCodes in Unity map directly to a - // physical key on an keyboard with an English layout. + // physical key on a keyboard with an English layout. // (2) Ctrl-Z and Ctrl-Y are reserved for Undo and Redo. // (3) The digits 0-9 are reserved for shortcuts for the player menu. @@ -30,6 +30,7 @@ internal static class KeyBindings /// /// Returns true if the user has pressed down a key requesting the given + /// in the last frame. /// /// the to check /// true if the user has pressed a key requesting the given diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs b/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs index 6d3d461c84..8543c927c2 100644 --- a/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs @@ -62,6 +62,12 @@ public partial class CodeWindow : BaseWindow /// private LSPHandler lspHandler; + /// + /// The handler for the context menu of this code window, which provides various navigation options, + /// such as "Go to Definition" or "Find References". + /// + private ContextMenuHandler contextMenu; + /// /// Path to the code window content prefab. /// @@ -118,13 +124,14 @@ public void MarkLine(int lineNumber) .ToArray(); if (lineNumber < 1) { - textMesh.text = string.Join("\n", allLines); + text = string.Join("\n", allLines); } else { string markLine = $"{markColor}{allLines[lineNumber - 1][markColorLength..]}"; - textMesh.text = string.Join("\n", allLines.Exclude(lineNumber - 1, 1).Insert(new[] { markLine }, lineNumber - 1)); + text = string.Join("\n", allLines.Exclude(lineNumber - 1, 1).Insert(new[] { markLine }, lineNumber - 1)); } + textMesh.text = text; } #region Visible Line Calculation @@ -184,7 +191,6 @@ public int ScrolledVisibleLine .AppendCallback(() => scrollingTo = 0); MarkLine(value); - ScrollEvent.Invoke(); } } diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs b/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs index 3d98edd746..0068e6869f 100644 --- a/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs @@ -6,6 +6,7 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Models; using SEE.Controls; using SEE.Controls.Actions; +using SEE.Tools.LSP; using SEE.UI.Menu; using SEE.UI.Notification; using SEE.UI.PopupMenu; @@ -19,105 +20,177 @@ namespace SEE.UI.Window.CodeWindow public partial class CodeWindow { /// - /// The context menu that this class manages. + /// A handler for the context menu of code windows that provides various navigation options, + /// such as "Go to Definition" or "Find References". /// - private PopupMenu.PopupMenu contextMenu; - - /// - /// A list menu that's shown when the user needs to make some selection, - /// such as when choosing a reference to navigate to. - /// - private SimpleListMenu simpleListMenu; - - /// - /// Shows the context menu at the given , assuming the user right-clicked - /// at the given and . - /// - /// The 0-indexed line where the user right-clicked. - /// The 0-indexed column where the user right-clicked. - /// The position where the context menu should be shown. - /// The word at the given and . - private void ShowContextMenu(int line, int column, Vector2 position, string contextWord) + /// The path of the file that is displayed in the code window. + /// The LSP handler that provides the language server capabilities. + /// A list menu that's shown when the user needs to make some selection, + /// such as when choosing a reference to navigate to. + /// The context menu that this class manages. + /// A callback that opens the given URI and range in a new code window + /// or scrolls to the range if the URI is the same as the current file. + private record ContextMenuHandler(string path, LSPHandler lspHandler, PopupMenu.PopupMenu contextMenu, + SimpleListMenu simpleListMenu, Action OpenSelection) { - IList actions = new List(); - - // TODO: Type Hierarchy and Call Hierarchy - if (lspHandler.ServerCapabilities.ReferencesProvider != null) + /// + /// Creates and initializes a new context menu handler for the given . + /// + /// The code window for which the context menu should be created. + /// The created context menu handler. + public static ContextMenuHandler FromCodeWindow(CodeWindow codeWindow) { - actions.Add(new("Show References", ShowReferences, Icons.IncomingEdge)); - } - if (lspHandler.ServerCapabilities.DeclarationProvider != null) - { - actions.Add(new("Go to Declaration", ShowDeclaration, Icons.OutgoingEdge)); - } - if (lspHandler.ServerCapabilities.DefinitionProvider != null) - { - actions.Add(new("Go to Definition", ShowDefinition, Icons.OutgoingEdge)); - } - if (lspHandler.ServerCapabilities.ImplementationProvider != null) - { - actions.Add(new("Go to Implementation", ShowImplementation, Icons.OutgoingEdge)); - } - if (lspHandler.ServerCapabilities.TypeDefinitionProvider != null) - { - actions.Add(new("Go to Type Definition", ShowTypeDefinition, Icons.OutgoingEdge)); - } - if (lspHandler.ServerCapabilities.CallHierarchyProvider != null) - { - actions.Add(new("Show Outgoing Calls", ShowOutgoingCalls, Icons.Sitemap)); - } - if (lspHandler.ServerCapabilities.TypeHierarchyProvider != null) - { - actions.Add(new("Show Supertypes", ShowSupertypes, Icons.Sitemap)); + PopupMenu.PopupMenu contextMenu = codeWindow.gameObject.AddComponent(); + SimpleListMenu simpleListMenu = codeWindow.gameObject.AddComponent(); + return new ContextMenuHandler(codeWindow.FilePath, codeWindow.lspHandler, contextMenu, + simpleListMenu, codeWindow.OpenSelection); } - if (actions.Count == 0) + /// + /// Shows the context menu at the given , assuming the user right-clicked + /// at the given and . + /// + /// The 0-indexed line where the user right-clicked. + /// The 0-indexed column where the user right-clicked. + /// The position where the context menu should be shown. + /// The word at the given and . + public void Show(int line, int column, Vector2 position, string contextWord) { - return; - } - contextMenu.ShowWith(actions, position); + IList actions = new List(); - return; + if (lspHandler.ServerCapabilities.ReferencesProvider != null) + { + actions.Add(new("Find References", WithLineColumn(ShowReferences), Icons.MagnifyingGlass)); + } + if (lspHandler.ServerCapabilities.DeclarationProvider != null) + { + actions.Add(new("Go to Declaration", WithLineColumn(ShowDeclaration), Icons.IncomingEdge)); + } + if (lspHandler.ServerCapabilities.DefinitionProvider != null) + { + actions.Add(new("Go to Definition", WithLineColumn(ShowDefinition), Icons.IncomingEdge)); + } + if (lspHandler.ServerCapabilities.TypeDefinitionProvider != null) + { + actions.Add(new("Go to Type Definition", WithLineColumn(ShowTypeDefinition), Icons.IncomingEdge)); + } + if (lspHandler.ServerCapabilities.ImplementationProvider != null) + { + actions.Add(new("Go to Implementation", WithLineColumn(ShowImplementation), Icons.OutgoingEdge)); + } + if (lspHandler.ServerCapabilities.CallHierarchyProvider != null) + { + actions.Add(new("Show Outgoing Calls", WithLineColumn(ShowOutgoingCalls), Icons.Sitemap)); + } + if (lspHandler.ServerCapabilities.TypeHierarchyProvider != null) + { + actions.Add(new("Show Supertypes", WithLineColumn(ShowSupertypes), Icons.Sitemap)); + } + + if (actions.Count > 0) + { + contextMenu.ShowWith(actions, position); + } + return; - #region Local Functions + // Calls the given action with the line, column, and context word given to Show. + Action WithLineColumn(Action action) => () => action(line, column, contextWord); + } - void ShowOutgoingCalls() + /// + /// Shows the outgoing calls for the given and . + /// + /// The 0-indexed line for which to show the outgoing calls. + /// The 0-indexed column for which to show the outgoing calls. + /// The word at the given and . + private void ShowOutgoingCalls(int line, int column, string contextWord) { - MenuEntriesForLocationsAsync(lspHandler.OutgoingCalls(_ => true, FilePath, line, column) - .Select(x => (x.Uri.ToUri(), Range.FromLspRange(x.Range), x.Name))) - .ContinueWith(entries => ShowEntries(entries, "Outgoing Calls")).Forget(); + const string name = "Outgoing Calls"; + MenuEntriesForLocationsAsync(lspHandler.OutgoingCalls(_ => true, path, line, column) + .Select(x => (x.Uri.ToUri(), Range.FromLspRange(x.Range), x.Name)), + name, contextWord) + .ContinueWith(entries => ShowEntries(entries, name, contextWord)).Forget(); } - void ShowSupertypes() + /// + /// Shows the outgoing calls for the given and . + /// + /// The 0-indexed line for which to show the outgoing calls. + /// The 0-indexed column for which to show the outgoing calls. + /// The word at the given and . + private void ShowSupertypes(int line, int column, string contextWord) { - MenuEntriesForLocationsAsync(lspHandler.Supertypes(_ => true, FilePath, line, column) - .Select(x => (x.Uri.ToUri(), Range.FromLspRange(x.Range), x.Name))) - .ContinueWith(entries => ShowEntries(entries, "Supertypes")).Forget(); + const string name = "Supertypes"; + MenuEntriesForLocationsAsync(lspHandler.Supertypes(_ => true, path, line, column) + .Select(x => (x.Uri.ToUri(), Range.FromLspRange(x.Range), x.Name)), + name, contextWord) + .ContinueWith(entries => ShowEntries(entries, name, contextWord)).Forget(); } - void ShowReferences() => - ShowLocationsAsync(lspHandler.References(FilePath, line, column, includeDeclaration: true), - "References").Forget(); + /// + /// Shows the references for the given and . + /// + /// The 0-indexed line for which to show the references. + /// The 0-indexed column for which to show the references. + /// The word at the given and . + private void ShowReferences(int line, int column, string contextWord) => + ShowLocationsAsync(lspHandler.References(path, line, column, includeDeclaration: true), "References", contextWord).Forget(); - void ShowDeclaration() => - ShowLocationsAsync(lspHandler.Declaration(FilePath, line, column), "Declarations").Forget(); + /// + /// Shows the declaration for the given and . + /// + /// The 0-indexed line for which to show the declaration. + /// The 0-indexed column for which to show the declaration. + /// The word at the given and . + private void ShowDeclaration(int line, int column, string contextWord) => + ShowLocationsAsync(lspHandler.Declaration(path, line, column), "Declarations", contextWord).Forget(); - void ShowDefinition() => - ShowLocationsAsync(lspHandler.Definition(FilePath, line, column), "Definitions").Forget(); + /// + /// Shows the definition for the given and . + /// + /// The 0-indexed line for which to show the definition. + /// The 0-indexed column for which to show the definition. + /// The word at the given and . + public void ShowDefinition(int line, int column, string contextWord) => + ShowLocationsAsync(lspHandler.Definition(path, line, column), "Definitions", contextWord).Forget(); - void ShowImplementation() => - ShowLocationsAsync(lspHandler.Implementation(FilePath, line, column), "Implementations").Forget(); + /// + /// Shows the implementation for the given and . + /// + /// The 0-indexed line for which to show the implementation. + /// The 0-indexed column for which to show the implementation. + /// The word at the given and . + private void ShowImplementation(int line, int column, string contextWord) => + ShowLocationsAsync(lspHandler.Implementation(path, line, column), "Implementations", contextWord).Forget(); - void ShowTypeDefinition() => - ShowLocationsAsync(lspHandler.TypeDefinition(FilePath, line, column), "Type Definitions").Forget(); + /// + /// Shows the type definition for the given and . + /// + /// The 0-indexed line for which to show the type definition. + /// The 0-indexed column for which to show the type definition. + /// The word at the given and . + private void ShowTypeDefinition(int line, int column, string contextWord) => + ShowLocationsAsync(lspHandler.TypeDefinition(path, line, column), "Type Definitions", contextWord).Forget(); - async UniTask ShowLocationsAsync(IUniTaskAsyncEnumerable locations, string name) + /// + /// Opens a menu for the given with the given , + /// letting the user choose one of the locations to navigate to. + /// + /// The locations to show in the menu. + /// The name of the locations, e.g. "Definitions". + /// The word for which the locations are shown. + private async UniTask ShowLocationsAsync(IUniTaskAsyncEnumerable locations, string name, string contextWord) { - IList entries = await MenuEntriesForLocationsAsync(locations.Select(DeconstructLocation)); - ShowEntries(entries, name); + IList entries = await MenuEntriesForLocationsAsync(locations.Select(DeconstructLocation), name, contextWord); + ShowEntries(entries, name, contextWord); } - (Uri, Range, string) DeconstructLocation(LocationOrLocationLink location) + /// + /// Deconstructs the given into a URI, range, and title. + /// + /// The location to deconstruct. + /// A tuple containing the URI, range, and title of the location. + private static (Uri, Range, string) DeconstructLocation(LocationOrLocationLink location) { Range targetRange; Uri targetUri; @@ -136,22 +209,29 @@ async UniTask ShowLocationsAsync(IUniTaskAsyncEnumerable return (targetUri, targetRange, null); } - // Generates menu entries for the given locations. Clicking on an entry will open the location. - async UniTask> MenuEntriesForLocationsAsync(IUniTaskAsyncEnumerable<(Uri, Range, string)> locations) + /// + /// Generates menu entries for the given . + /// Clicking on an entry will invoke . + /// + /// The locations to generate menu entries for. + /// The name of the locations, e.g. "Definitions". + /// The word for which the locations are shown. + /// The generated menu entries. + private async UniTask> MenuEntriesForLocationsAsync(IUniTaskAsyncEnumerable<(Uri, Range, string)> locations, string name, string contextWord) { IList entries = new List(); using (LoadingSpinner.ShowIndeterminate($"Loading {name} for \"{contextWord}\"...")) { await foreach ((Uri targetUri, Range targetRange, string title) in locations) { - Uri path = targetUri; - if (lspHandler.ProjectUri?.IsBaseOf(path) ?? false) + Uri uri = targetUri; + if (lspHandler.ProjectUri?.IsBaseOf(uri) ?? false) { // Truncate path above the project's base path to make the result more readable. - path = lspHandler.ProjectUri.MakeRelativeUri(path); + uri = lspHandler.ProjectUri.MakeRelativeUri(uri); } entries.Add(new(SelectAction: () => OpenSelection(targetUri, targetRange), - Title: title ?? $"{path}: {targetRange}", + Title: title ?? $"{uri}: {targetRange}", // TODO: Icon EntryColor: new Color(0.051f, 0.3608f, 0.1333f))); } @@ -160,8 +240,15 @@ async UniTask> MenuEntriesForLocationsAsync(IUniTaskAsyncEnumer return entries; } - // Opens a menu with the given menu entries. - void ShowEntries(IList entries, string name) + /// + /// Opens a menu with the given . + /// If there are no entries, a notification is shown informing the user that there are no results. + /// If there is only one entry, it is directly opened. + /// + /// The entries to show in the menu. + /// The name of the entries, e.g. "Definitions". + /// The word for which the entries are shown. + private void ShowEntries(IList entries, string name, string contextWord) { switch (entries.Count) { @@ -183,36 +270,41 @@ void ShowEntries(IList entries, string name) break; } } + } - // Opens the selection at the given uri and range. If the uri is the current file, we just - // scroll to the range, otherwise we open a new (or existing) code window. - void OpenSelection(Uri uri, Range range) + /// + /// Opens the selection at the given and . + /// If the URI is the same as the current file, the code window is scrolled to the range, + /// otherwise a new code window is opened. + /// + /// The URI of the file to open. + /// The range to scroll to or show in the new code window. + private void OpenSelection(Uri uri, Range range) + { + // When we're going somewhere else, we should deactivate the current tooltip first. + Tooltip.Deactivate(); + if (FilePath == uri.LocalPath) { // If this is the current file, we can just scroll to the range. - if (FilePath == uri.LocalPath) - { - // TODO: Allow going back (button or keyboard shortcut) - ScrolledVisibleLine = range.Start.Line; - } - else + ScrolledVisibleLine = range.Start.Line; + } + else + { + // Otherwise, we need to open a different code window. + Assert.IsNotNull(AssociatedGraph); + CodeWindow window = ShowCodeAction.ShowCodeForPath(AssociatedGraph, uri.LocalPath, range, + w => w.ScrolledVisibleLine = range.Start.Line); + if (window != null) { - // Otherwise, we need to open a different code window. - Assert.IsNotNull(AssociatedGraph); - CodeWindow window = ShowCodeAction.ShowCodeForPath(AssociatedGraph, uri.LocalPath, range, - w => w.ScrolledVisibleLine = range.Start.Line); - if (window != null) + WindowSpace manager = WindowSpaceManager.ManagerInstance[WindowSpaceManager.LocalPlayer]; + if (!manager.Windows.Contains(window)) { - WindowSpace manager = WindowSpaceManager.ManagerInstance[WindowSpaceManager.LocalPlayer]; - if (!manager.Windows.Contains(window)) - { - manager.AddWindow(window); - } - manager.ActiveWindow = window; + manager.AddWindow(window); } + manager.ActiveWindow = window; } } - - #endregion } + } } diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs b/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs index eba88bd439..45933e523d 100644 --- a/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs @@ -544,7 +544,7 @@ private static async UniTask> GetDashboardIssuesAsync(string path) } catch (DashboardException e) { - ShowNotification.Error("Couldn't load issues", e.Message); + ShowNotification.Error("Couldn't load dashboard issues", e.Message); return new List(); } diff --git a/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs b/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs index d846c8d1b3..13650c83be 100644 --- a/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs +++ b/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs @@ -16,7 +16,6 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Models; using SEE.Controls; using SEE.UI.DebugAdapterProtocol; -using SEE.UI.Menu; using SEE.Utils.Markdown; using UnityEngine.Assertions; @@ -55,11 +54,10 @@ protected override void StartDesktop() scrollable = PrefabInstantiator.InstantiatePrefab(codeWindowPrefab, Window.transform.Find("Content"), false); scrollable.name = "Scrollable"; - // Initialize list and context menu, if necessary. + // Initialize context menu, if necessary. if (lspHandler != null) { - contextMenu = gameObject.AddComponent(); - simpleListMenu = gameObject.AddComponent(); + contextMenu = ContextMenuHandler.FromCodeWindow(this); } // Set text and preferred font size @@ -181,7 +179,7 @@ protected override void UpdateDesktop() { int character = word.firstCharacterIndex; (int line, int column) = GetSourcePosition(character); - ShowContextMenu(line-1, column-1, Input.mousePosition, word.GetWord()); + contextMenu.Show(line - 1, column - 1, Input.mousePosition, word.GetWord()); } return; } @@ -199,8 +197,8 @@ protected override void UpdateDesktop() } TMP_WordInfo? hoveredWord = DetectHoveredWord(); - // Detect hovering over words - if (!lastHoveredWord.Equals(hoveredWord)) + // Detect hovering over words (only while the code window is not being scrolled). + if (scrollingTo == 0 && !lastHoveredWord.Equals(hoveredWord)) { if (lspHandler != null) { @@ -210,14 +208,44 @@ protected override void UpdateDesktop() if (lastHoveredWord != null) { OnWordHoverEnd?.Invoke(this, lastHoveredWord.Value); + RemoveUnderline(lastHoveredWord.Value); } if (hoveredWord != null) { OnWordHoverBegin?.Invoke(this, hoveredWord.Value); + + // NOTE: We are not using SEEInput because: + // a) Any reasonable key here would conflict with our existing set of keys. + // We would need to implement context-dependent key bindings first. + // b) We need to differentiate between "key is in a pressed state", "key was pressed", + // and "key was released", which goes beyond the general interface of SEEInput. + if (Input.GetKey(KeyCode.LeftControl)) + { + UnderlineHoveredWord(hoveredWord.Value); + } } + + lastHoveredWord = hoveredWord; + } + else if (Input.GetKeyUp(KeyCode.LeftControl) && lastHoveredWord != null) + { + RemoveUnderline(lastHoveredWord.Value); + } + else if (Input.GetKeyDown(KeyCode.LeftControl) && lastHoveredWord != null) + { + UnderlineHoveredWord(lastHoveredWord.Value); + } + + if (Input.GetMouseButton(0) && Input.GetKey(KeyCode.LeftControl) && lastHoveredWord != null) + { + RemoveUnderline(lastHoveredWord.Value); + GoToDefinition(lastHoveredWord.Value); } - lastHoveredWord = hoveredWord; } + + const string startUnderline = ""; + const string endUnderline = ""; + return; TMP_WordInfo? DetectHoveredWord() @@ -225,6 +253,44 @@ protected override void UpdateDesktop() int index = TMP_TextUtilities.FindIntersectingWord(textMesh, Input.mousePosition, null); return index >= 0 && index < textMesh.textInfo.wordCount ? textMesh.textInfo.wordInfo[index] : null; } + + void UnderlineHoveredWord(TMP_WordInfo word) + { + int start = textMesh.textInfo.characterInfo[word.firstCharacterIndex].index; + int end = textMesh.textInfo.characterInfo[word.lastCharacterIndex].index + 1; + // We need to change the rich text tags to underline the word. + textMesh.text = text = text[..start] + startUnderline + text[start..end] + endUnderline + text[end..]; + textMesh.ForceMeshUpdate(); + } + + void RemoveUnderline(TMP_WordInfo word) + { + // Start and end characters do not include the underline tags (if they exist), + // so we need to adjust them. + if (word.lastCharacterIndex >= textMesh.textInfo.characterCount) + { + return; + } + int start = textMesh.textInfo.characterInfo[word.firstCharacterIndex].index - startUnderline.Length; + int end = textMesh.textInfo.characterInfo[word.lastCharacterIndex].index + 1 + endUnderline.Length; + + if (start >= 0 && end <= text.Length) + { + string underlinedPart = text[start..end]; + if (underlinedPart.StartsWith(startUnderline) && underlinedPart.EndsWith(endUnderline)) + { + text = text[..start] + underlinedPart[startUnderline.Length..^endUnderline.Length] + text[end..]; + textMesh.text = text; + textMesh.ForceMeshUpdate(); + } + } + } + + void GoToDefinition(TMP_WordInfo word) + { + (int line, int column) = GetSourcePosition(word.firstCharacterIndex); + contextMenu.ShowDefinition(line - 1, column - 1, word.GetWord()); + } } /// diff --git a/Assets/SEE/Utils/Icons.cs b/Assets/SEE/Utils/Icons.cs index 705e3f0290..370a6abc72 100644 --- a/Assets/SEE/Utils/Icons.cs +++ b/Assets/SEE/Utils/Icons.cs @@ -43,5 +43,6 @@ public static class Icons public const char CircleQuestionMark = '\uF059'; public const char CircleExclamationMark = '\uF06A'; public const char Sitemap = '\uF0E8'; + public const char MagnifyingGlass = '\uF002'; } } From e9119352bade908636ba0da85112880671db4bf0 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Sat, 27 Jul 2024 20:00:57 +0200 Subject: [PATCH 17/23] Replace Sprite icons with FA icons in menu entries This gives us a much easier option of using existing icons, rather than having to import an image and convert it to a sprite each time. It also improves performance a bit. --- Assets/Resources/Prefabs/UI/Button.prefab | 181 ++++++++++++------ Assets/Resources/Prefabs/UI/Menu.prefab | 104 +++++----- .../Actions/AbstractActionStateType.cs | 13 +- .../SEE/Controls/Actions/ActionStateType.cs | 6 +- .../Controls/Actions/ActionStateTypeGroup.cs | 6 +- .../SEE/Controls/Actions/ActionStateTypes.cs | 44 ++--- Assets/SEE/GameObjects/Menu/PlayerMenu.cs | 4 +- Assets/SEE/UI/HelpSystem/HelpSystemBuilder.cs | 18 +- Assets/SEE/UI/HelpSystem/HelpSystemEntry.cs | 1 - .../UI/Menu/Desktop/SimpleListMenuDesktop.cs | 15 +- Assets/SEE/UI/Menu/MenuEntry.cs | 6 +- Assets/SEE/UI/Menu/NestedMenu.cs | 5 +- Assets/SEE/UI/Menu/NestedMenuEntry.cs | 16 +- Assets/SEE/UI/OpeningDialog.cs | 8 +- .../SEE/UI/PropertyDialog/ButtonProperty.cs | 15 +- .../UI/RuntimeConfigMenu/RuntimeTabMenu.cs | 2 +- .../CodeWindow/CodeWindowContextMenu.cs | 2 +- Assets/SEE/Utils/Icons.cs | 66 ++++--- Assets/SEEPlayModeTests/TestMenu.cs | 17 +- Assets/SEEPlayModeTests/TestNestedMenu.cs | 10 +- Assets/SEEPlayModeTests/TestSimpleMenu.cs | 6 +- Assets/SEETests/TestActionHistory.cs | 4 +- Assets/SEETests/TestActionStateType.cs | 6 +- Assets/SEETests/UI/TestMenuEntry.cs | 42 ++-- Assets/SEETests/UI/TestToggleMenuEntry.cs | 2 +- 25 files changed, 341 insertions(+), 258 deletions(-) diff --git a/Assets/Resources/Prefabs/UI/Button.prefab b/Assets/Resources/Prefabs/UI/Button.prefab index 8f56791638..8e8c9d65ce 100644 --- a/Assets/Resources/Prefabs/UI/Button.prefab +++ b/Assets/Resources/Prefabs/UI/Button.prefab @@ -28,9 +28,9 @@ RectTransform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 6586710328604211363} - m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 1, y: 1} @@ -65,10 +65,11 @@ MonoBehaviour: m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_text: BUTTON + m_text: Button m_isRightToLeft: 0 m_fontAsset: {fileID: 11400000, guid: 84dd14695854bbc43a5faa24fcf93d0d, type: 2} - m_sharedMaterial: {fileID: 21261991626553910, guid: 84dd14695854bbc43a5faa24fcf93d0d, type: 2} + m_sharedMaterial: {fileID: 21261991626553910, guid: 84dd14695854bbc43a5faa24fcf93d0d, + type: 2} m_fontSharedMaterials: [] m_fontMaterial: {fileID: 0} m_fontMaterials: [] @@ -144,7 +145,7 @@ GameObject: m_Component: - component: {fileID: 8426220730825732241} - component: {fileID: 6175946636808426846} - - component: {fileID: 1531332202938432071} + - component: {fileID: 8382553333224995676} m_Layer: 5 m_Name: Icon m_TagString: Untagged @@ -162,9 +163,9 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 6586710328604211363} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0.5} m_AnchorMax: {x: 0, y: 0.5} @@ -179,7 +180,7 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 4744790295661001541} m_CullTransparentMesh: 0 ---- !u!114 &1531332202938432071 +--- !u!114 &8382553333224995676 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -188,27 +189,87 @@ MonoBehaviour: m_GameObject: {fileID: 4744790295661001541} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} - m_Color: {r: 0.1764706, g: 0.25490198, b: 0.33333334, a: 1} + m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_Sprite: {fileID: 21300000, guid: a8cc5f0db692cb24db144d85c01b6838, type: 3} - m_Type: 0 - m_PreserveAspect: 1 - m_FillCenter: 1 - m_FillMethod: 4 - m_FillAmount: 1 - m_FillClockwise: 1 - m_FillOrigin: 0 - m_UseSpriteMesh: 0 - m_PixelsPerUnitMultiplier: 1 + m_text: '?' + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 4ebb98a3c87fa521a888029274c92b79, type: 2} + m_sharedMaterial: {fileID: -8620075009897487826, guid: 4ebb98a3c87fa521a888029274c92b79, + type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4278190080 + m_fontColor: {r: 0, g: 0, b: 0, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 30 + m_fontSizeBase: 30 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_enableWordWrapping: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 1 + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} --- !u!1 &4918872672529199024 GameObject: m_ObjectHideFlags: 0 @@ -238,9 +299,9 @@ RectTransform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 6586710328604211363} - m_RootOrder: 2 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 1, y: 1} @@ -308,10 +369,10 @@ GameObject: m_Component: - component: {fileID: 6586710328604211363} - component: {fileID: 2917540488982011779} - - component: {fileID: 8210310399375922163} - component: {fileID: 2539900339822752133} - component: {fileID: 436568625700612899} - component: {fileID: 6827147320954825169} + - component: {fileID: 5444878965105383002} m_Layer: 5 m_Name: Button m_TagString: Untagged @@ -329,12 +390,12 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 8426220730825732241} - {fileID: 7086379228738207378} - {fileID: 6595404481868969509} m_Father: {fileID: 0} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5} @@ -349,46 +410,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 7702164899860693017} m_CullTransparentMesh: 0 ---- !u!114 &8210310399375922163 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 7702164899860693017} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: ddd611441fc6f1f4c9f714fcd4dcbeb3, type: 3} - m_Name: - m_EditorClassIdentifier: - buttonIcon: {fileID: 21300000, guid: a8cc5f0db692cb24db144d85c01b6838, type: 3} - buttonText: BUTTON - clickEvent: - m_PersistentCalls: - m_Calls: [] - hoverEvent: - m_PersistentCalls: - m_Calls: [] - hoverSound: {fileID: 0} - clickSound: {fileID: 0} - buttonVar: {fileID: 0} - normalImage: {fileID: 1531332202938432071} - normalText: {fileID: 7271999502370158117} - soundSource: {fileID: 0} - rippleParent: {fileID: 4918872672529199024} - useCustomContent: 0 - enableButtonSounds: 0 - useHoverSound: 1 - useClickSound: 1 - useRipple: 1 - rippleUpdateMode: 1 - rippleShape: {fileID: 21300000, guid: d25e2ce15dd1c67438e4b70f404fb197, type: 3} - speed: 2.4 - maxSize: 6 - startColor: {r: 0, g: 0, b: 0, a: 0.39215687} - transitionColor: {r: 0, g: 0, b: 0, a: 0} - renderOnTop: 0 - centered: 0 --- !u!114 &2539900339822752133 MonoBehaviour: m_ObjectHideFlags: 0 @@ -475,3 +496,41 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 52dd8aaa3b5d4058ac1b1e9242d35f57, type: 3} m_Name: m_EditorClassIdentifier: +--- !u!114 &5444878965105383002 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7702164899860693017} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1a12ddbc47b17cd478cb447d1113a22b, type: 3} + m_Name: + m_EditorClassIdentifier: + buttonText: Button + clickEvent: + m_PersistentCalls: + m_Calls: [] + hoverEvent: + m_PersistentCalls: + m_Calls: [] + hoverSound: {fileID: 0} + clickSound: {fileID: 0} + buttonVar: {fileID: 0} + normalText: {fileID: 7271999502370158117} + soundSource: {fileID: 0} + rippleParent: {fileID: 4918872672529199024} + useCustomContent: 0 + enableButtonSounds: 0 + useHoverSound: 1 + useClickSound: 1 + useRipple: 1 + rippleUpdateMode: 1 + rippleShape: {fileID: 21300000, guid: d25e2ce15dd1c67438e4b70f404fb197, type: 3} + speed: 2.4 + maxSize: 6 + startColor: {r: 1, g: 1, b: 1, a: 1} + transitionColor: {r: 1, g: 1, b: 1, a: 0} + renderOnTop: 0 + centered: 0 diff --git a/Assets/Resources/Prefabs/UI/Menu.prefab b/Assets/Resources/Prefabs/UI/Menu.prefab index 14f9025144..e11a1fb41a 100644 --- a/Assets/Resources/Prefabs/UI/Menu.prefab +++ b/Assets/Resources/Prefabs/UI/Menu.prefab @@ -55,8 +55,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -130,8 +130,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -206,8 +206,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Padding: m_Left: 0 m_Right: 0 @@ -282,8 +282,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: ef1b9ec3e5bcdc64d87d311ffc627a77, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: buttonIcon: {fileID: 21300000, guid: 5d08ed2465c5c104c9c915959d69b527, type: 3} buttonText: CLOSE clickEvent: @@ -326,8 +326,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Navigation: m_Mode: 0 m_WrapAround: 0 @@ -361,7 +361,7 @@ MonoBehaviour: m_PersistentCalls: m_Calls: - m_Target: {fileID: 2057475846036032719} - m_TargetAssemblyTypeName: + m_TargetAssemblyTypeName: m_MethodName: CloseWindow m_Mode: 1 m_Arguments: @@ -369,7 +369,7 @@ MonoBehaviour: m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine m_IntArgument: 0 m_FloatArgument: 0 - m_StringArgument: + m_StringArgument: m_BoolArgument: 0 m_CallState: 2 --- !u!95 &1753449128007808263 @@ -388,7 +388,7 @@ Animator: m_ApplyRootMotion: 0 m_LinearVelocityBlending: 0 m_StabilizeFeet: 0 - m_WarningMessage: + m_WarningMessage: m_HasTransformHierarchy: 1 m_AllowConstantClipSamplingOptimization: 1 m_KeepAnimatorStateOnDisable: 0 @@ -403,8 +403,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 0} m_RaycastTarget: 1 @@ -478,8 +478,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -615,8 +615,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Padding: m_Left: 0 m_Right: 0 @@ -689,8 +689,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -719,8 +719,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 31a19414c41e5ae4aae2af33fee712f6, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_ShowMaskGraphic: 0 --- !u!225 &8644951742806170891 CanvasGroup: @@ -789,8 +789,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -927,8 +927,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -1017,8 +1017,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_HorizontalFit: 2 m_VerticalFit: 0 --- !u!1 &2574983841249248955 @@ -1076,8 +1076,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -1155,8 +1155,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 8ff5b50d8ff89864090b86d1fee33b66, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: windowIcon: {fileID: 1059299024181343886} windowTitle: {fileID: 196948630522393265} windowDescription: {fileID: 5106170833735170336} @@ -1205,7 +1205,7 @@ Animator: m_ApplyRootMotion: 0 m_LinearVelocityBlending: 0 m_StabilizeFeet: 0 - m_WarningMessage: + m_WarningMessage: m_HasTransformHierarchy: 1 m_AllowConstantClipSamplingOptimization: 1 m_KeepAnimatorStateOnDisable: 0 @@ -1265,8 +1265,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -1340,8 +1340,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0.37254903, g: 0.40784317, b: 0.45098042, a: 1} m_RaycastTarget: 1 @@ -1534,8 +1534,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Padding: m_Left: 0 m_Right: 0 @@ -1608,8 +1608,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0, g: 0, b: 0, a: 0.078431375} m_RaycastTarget: 1 @@ -1650,8 +1650,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 31a19414c41e5ae4aae2af33fee712f6, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_ShowMaskGraphic: 0 --- !u!1 &7199230629601579101 GameObject: @@ -1712,8 +1712,8 @@ MonoBehaviour: m_Enabled: 0 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -1787,8 +1787,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -1982,8 +1982,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -2057,8 +2057,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0.078431375, g: 0.09803922, b: 0.11764706, a: 0.9607843} m_RaycastTarget: 1 diff --git a/Assets/SEE/Controls/Actions/AbstractActionStateType.cs b/Assets/SEE/Controls/Actions/AbstractActionStateType.cs index 8260f35ee6..fd3c2cd50d 100644 --- a/Assets/SEE/Controls/Actions/AbstractActionStateType.cs +++ b/Assets/SEE/Controls/Actions/AbstractActionStateType.cs @@ -25,11 +25,11 @@ public abstract class AbstractActionStateType public Color Color { get; } /// - /// Path to the material of the icon for this action. + /// The FontAwesome codepoint of the icon for this action. See for more information. /// The icon itself should be a visual representation of the action. /// Will be used in the . /// - public string IconPath { get; } + public char Icon { get; } /// /// The parent of this action state type, i.e., the @@ -47,16 +47,17 @@ public abstract class AbstractActionStateType /// The Name of this ActionStateType. Must be unique. /// Description for this ActionStateType. /// Color for this ActionStateType. - /// Path to the material of the icon for this ActionStateType. + /// The icon which shall be displayed alongside this entry, + /// given as a FontAwesome codepoint. /// The group this action state type belongs to; may be null. /// If true, this action state type will be registered in . - protected AbstractActionStateType - (string name, string description, Color color, string iconPath, ActionStateTypeGroup group, bool register) + protected AbstractActionStateType(string name, string description, Color color, char icon, + ActionStateTypeGroup group, bool register) { Name = name; Description = description; Color = color; - IconPath = iconPath; + Icon = icon; group?.Add(this); if (register) { diff --git a/Assets/SEE/Controls/Actions/ActionStateType.cs b/Assets/SEE/Controls/Actions/ActionStateType.cs index 64b3ee4e19..a03f66515a 100644 --- a/Assets/SEE/Controls/Actions/ActionStateType.cs +++ b/Assets/SEE/Controls/Actions/ActionStateType.cs @@ -24,13 +24,13 @@ public class ActionStateType : AbstractActionStateType /// Description for this ActionStateType. /// The parent of this action in the nesting hierarchy in the menu. /// Color for this ActionStateType. - /// Path to the material of the icon for this ActionStateType. + /// Icon for this ActionStateType. /// Delegate to be called to create a new instance of this kind of action. /// Can be null, in which case no delegate will be called. /// If true, this action state type will be registered in . public ActionStateType(string name, string description, - Color color, string iconPath, CreateReversibleAction createReversible, ActionStateTypeGroup parent = null, bool register = true) - : base(name, description, color, iconPath, parent, register) + Color color, char icon, CreateReversibleAction createReversible, ActionStateTypeGroup parent = null, bool register = true) + : base(name, description, color, icon, parent, register) { CreateReversible = createReversible; } diff --git a/Assets/SEE/Controls/Actions/ActionStateTypeGroup.cs b/Assets/SEE/Controls/Actions/ActionStateTypeGroup.cs index d36ca9104c..238f6aaeb6 100644 --- a/Assets/SEE/Controls/Actions/ActionStateTypeGroup.cs +++ b/Assets/SEE/Controls/Actions/ActionStateTypeGroup.cs @@ -17,11 +17,11 @@ public class ActionStateTypeGroup : AbstractActionStateType /// Description for this . /// The parent of this action in the nesting hierarchy in the menu. /// Color for this . - /// Path to the material of the icon for this . + /// Icon for this , given as a FontAwesome codepoint. /// If true, this action state type will be registered in . public ActionStateTypeGroup - (string name, string description, Color color, string iconPath, ActionStateTypeGroup parent = null, bool register = true) - : base(name, description, color, iconPath, parent, register) + (string name, string description, Color color, char icon, ActionStateTypeGroup parent = null, bool register = true) + : base(name, description, color, icon, parent, register) { } diff --git a/Assets/SEE/Controls/Actions/ActionStateTypes.cs b/Assets/SEE/Controls/Actions/ActionStateTypes.cs index 4edc9268e5..9be0a8cf70 100644 --- a/Assets/SEE/Controls/Actions/ActionStateTypes.cs +++ b/Assets/SEE/Controls/Actions/ActionStateTypes.cs @@ -63,109 +63,109 @@ static ActionStateTypes() { Move = new("Move", "Move a node within a graph", - Color.red.Darker(), "Materials/Charts/MoveIcon", + Color.red.Darker(), Icons.Move, MoveAction.CreateReversibleAction); Rotate = new("Rotate", "Rotate the selected node and its children within a graph", - Color.blue.Darker(), "Materials/ModernUIPack/Refresh", + Color.blue.Darker(), Icons.Rotate, RotateAction.CreateReversibleAction); Hide = new("Hide", "Hides nodes or edges", - Color.yellow.Darker(), "Materials/ModernUIPack/Eye", + Color.yellow.Darker(), Icons.EyeSlash, HideAction.CreateReversibleAction); NewEdge = new("New Edge", "Draw a new edge between two nodes", - Color.green.Darker(), "Materials/ModernUIPack/Minus", + Color.green.Darker(), Icons.Edge, AddEdgeAction.CreateReversibleAction); NewNode = new("New Node", "Create a new node", - Color.green.Darker(), "Materials/ModernUIPack/Plus", + Color.green.Darker(), '+', AddNodeAction.CreateReversibleAction); EditNode = new("Edit Node", "Edit a node", - Color.green.Darker(), "Materials/ModernUIPack/Settings", + Color.green.Darker(), Icons.PenToSquare, EditNodeAction.CreateReversibleAction); ScaleNode = new("Scale Node", "Scale a node", - Color.green.Darker(), "Materials/ModernUIPack/Crop", + Color.green.Darker(), Icons.Scale, ScaleNodeAction.CreateReversibleAction); Delete = new("Delete", "Delete a node or an edge", - Color.yellow.Darker(), "Materials/ModernUIPack/Trash", + Color.yellow.Darker(), Icons.Trash, DeleteAction.CreateReversibleAction); ShowCode = new("Show Code", "Display the source code of a node.", - Color.black, "Materials/ModernUIPack/Document", + Color.black, Icons.Code, ShowCodeAction.CreateReversibleAction); Draw = new("Draw", "Draw a line", - Color.magenta.Darker(), "Materials/ModernUIPack/Pencil", + Color.magenta.Darker(), Icons.Pencil, DrawAction.CreateReversibleAction); AcceptDivergence = new("Accept Divergence", "Accept a diverging edge into the architecture", - Color.grey.Darker(), "Materials/ModernUIPack/Arrow Bold", + Color.grey.Darker(), Icons.CheckedCheckbox, AcceptDivergenceAction.CreateReversibleAction); // Metric Board actions MetricBoard = - new("Metric Board", "Manipulate a metric board", - Color.white.Darker(), "Materials/ModernUIPack/Pencil"); + new("Metric Board", "Manipulate a metric board", + Color.white.Darker(), Icons.Chalkboard); AddBoard = new("Add Board", "Add a board", - Color.green.Darker(), "Materials/ModernUIPack/Plus", + Color.green.Darker(), '+', AddBoardAction.CreateReversibleAction, parent: MetricBoard); AddWidget = new("Add Widget", "Add a widget", - Color.green.Darker(), "Materials/ModernUIPack/Plus", + Color.green.Darker(), '+', AddWidgetAction.CreateReversibleAction, parent: MetricBoard); MoveBoard = new("Move Board", "Move a board", - Color.yellow.Darker(), "Materials/Charts/MoveIcon", + Color.yellow.Darker(), Icons.Move, MoveBoardAction.CreateReversibleAction, parent: MetricBoard); MoveWidget = new("Move Widget", "Move a widget", - Color.yellow.Darker(), "Materials/Charts/MoveIcon", + Color.yellow.Darker(), Icons.Move, MoveWidgetAction.CreateReversibleAction, parent: MetricBoard); DeleteBoard = new("Delete Board", "Delete a board", - Color.red.Darker(), "Materials/ModernUIPack/Trash", + Color.red.Darker(), Icons.Trash, DeleteBoardAction.CreateReversibleAction, parent: MetricBoard); DeleteWidget = new("Delete Widget", "Delete a widget", - Color.red.Darker(), "Materials/ModernUIPack/Trash", + Color.red.Darker(), Icons.Trash, DeleteWidgetAction.CreateReversibleAction, parent: MetricBoard); LoadBoard = new("Load Board", "Load a board", - Color.blue.Darker(), "Materials/ModernUIPack/Document", + Color.blue.Darker(), Icons.Import, LoadBoardAction.CreateReversibleAction, parent: MetricBoard); SaveBoard = new("Save Board", "Save a board", - Color.blue.Darker(), "Materials/ModernUIPack/Document", + Color.blue.Darker(), Icons.Export, SaveBoardAction.CreateReversibleAction, parent: MetricBoard); } @@ -196,7 +196,7 @@ static ActionStateTypes() #endregion /// - /// Dumps all elements in - + /// Dumps all elements in . /// Can be used for debugging. /// public static void Dump() diff --git a/Assets/SEE/GameObjects/Menu/PlayerMenu.cs b/Assets/SEE/GameObjects/Menu/PlayerMenu.cs index 89e999d01f..4331bc8f85 100644 --- a/Assets/SEE/GameObjects/Menu/PlayerMenu.cs +++ b/Assets/SEE/GameObjects/Menu/PlayerMenu.cs @@ -89,7 +89,7 @@ bool Visit(AbstractActionStateType child, AbstractActionStateType parent) Title: actionStateType.Name, Description: actionStateType.Description, EntryColor: actionStateType.Color, - Icon: Resources.Load(actionStateType.IconPath)); + Icon: actionStateType.Icon); entry = menuEntry; } else if (child is ActionStateTypeGroup actionStateTypeGroup) @@ -98,7 +98,7 @@ bool Visit(AbstractActionStateType child, AbstractActionStateType parent) title: actionStateTypeGroup.Name, description: actionStateTypeGroup.Description, entryColor: actionStateTypeGroup.Color, - icon: Resources.Load(actionStateTypeGroup.IconPath)); + icon: actionStateTypeGroup.Icon); toNestedMenuEntry[actionStateTypeGroup] = nestedMenuEntry; entry = nestedMenuEntry; } diff --git a/Assets/SEE/UI/HelpSystem/HelpSystemBuilder.cs b/Assets/SEE/UI/HelpSystem/HelpSystemBuilder.cs index b153927e36..9d1b7f7025 100644 --- a/Assets/SEE/UI/HelpSystem/HelpSystemBuilder.cs +++ b/Assets/SEE/UI/HelpSystem/HelpSystemBuilder.cs @@ -18,6 +18,7 @@ using System.Collections.Generic; using System.IO; using SEE.UI.Menu; +using SEE.Utils; using TMPro; using UnityEngine; using UnityEngine.Video; @@ -107,25 +108,20 @@ private static GameObject HelpSystemObject() } /// - /// The path to the default-icon for an HelpSystemEntry in the nested menu. + /// The path to the default-icon for a in the nested menu. /// - private const string entryIcon = "Materials/ModernUIPack/Eye"; + private const char entryIcon = Icons.Eye; /// - /// The path to the default-icon for an RefEntry in the nested menu. + /// The path to the default-icon for a RefEntry in the nested menu. /// - private const string refIcon = "Materials/ModernUIPack/Plus"; + private const char refIcon = '+'; /// /// The LinkedListEntries of the currently selected HelpSystemEntry. /// public static LinkedList CurrentEntries; - /// - /// The space where the entry is inside. - /// - public static GameObject EntrySpace; - /// /// The headline gameObject of the helpSystemEntry or rather the headline which is inside of the dynamicPanel. /// @@ -154,7 +150,7 @@ public static MenuEntry CreateNewHelpSystemEntry Title: title, Description: description, EntryColor: entryColor, - Icon: Resources.Load(entryIcon)); + Icon: entryIcon); } /// @@ -173,7 +169,7 @@ public static NestedMenuEntry CreateNewRefEntry(List inner title: title, description: description, entryColor: entryColor, - icon: Resources.Load(refIcon)); + icon: refIcon); } /// diff --git a/Assets/SEE/UI/HelpSystem/HelpSystemEntry.cs b/Assets/SEE/UI/HelpSystem/HelpSystemEntry.cs index 6802db22e4..ebc3efd8de 100644 --- a/Assets/SEE/UI/HelpSystem/HelpSystemEntry.cs +++ b/Assets/SEE/UI/HelpSystem/HelpSystemEntry.cs @@ -245,7 +245,6 @@ public void ShowEntry() { helpSystemSpace = PrefabInstantiator.InstantiatePrefab(helpSystemEntrySpacePrefab, Canvas.transform, false); helpSystemEntry = PrefabInstantiator.InstantiatePrefab(helpSystemEntryPrefab, helpSystemSpace.transform, false); - HelpSystemBuilder.EntrySpace = helpSystemSpace; helpSystemSpace.transform.localScale = new Vector3(1.7f, 1.7f); RectTransform dynamicPanel = helpSystemSpace.transform.GetChild(2).GetComponent(); dynamicPanel.sizeDelta = new Vector2(550, 425); diff --git a/Assets/SEE/UI/Menu/Desktop/SimpleListMenuDesktop.cs b/Assets/SEE/UI/Menu/Desktop/SimpleListMenuDesktop.cs index 03eb55b239..17dcad2146 100644 --- a/Assets/SEE/UI/Menu/Desktop/SimpleListMenuDesktop.cs +++ b/Assets/SEE/UI/Menu/Desktop/SimpleListMenuDesktop.cs @@ -1,7 +1,9 @@ using System.Linq; using Michsky.UI.ModernUIPack; +using SEE.GO; using SEE.Utils; using Sirenix.Utilities; +using TMPro; using UnityEngine; using UnityEngine.UI; @@ -53,7 +55,7 @@ public partial class SimpleListMenu where T : MenuEntry /// /// The menu entry. /// The game object of the entry. - public GameObject EntryGameObject(T entry) => EntryList.transform.Cast().FirstOrDefault(x => x.name == entry.Title)?.gameObject; + protected GameObject EntryGameObject(T entry) => EntryList.transform.Cast().FirstOrDefault(x => x.name == entry.Title)?.gameObject; /// /// Initializes the menu. @@ -111,12 +113,13 @@ protected virtual void AddButton(T entry) // title and icon button.name = entry.Title; - ButtonManagerBasicWithIcon buttonManager = button.GetComponent(); + ButtonManagerBasic buttonManager = button.MustGetComponent(); + TextMeshProUGUI iconText = button.transform.Find("Icon").gameObject.MustGetComponent(); buttonManager.buttonText = entry.Title; - buttonManager.buttonIcon = entry.Icon; + iconText.text = entry.Icon.ToString(); // hover listeners - PointerHelper pointerHelper = button.GetComponent(); + PointerHelper pointerHelper = button.MustGetComponent(); if (entry.Description != null) { pointerHelper.EnterEvent.AddListener(_ => Tooltip.ActivateWith(entry.Description)); @@ -135,10 +138,10 @@ protected virtual void AddButton(T entry) // colors Color color = entry.Enabled ? entry.EntryColor : entry.DisabledColor; - button.GetComponent().color = color; + button.MustGetComponent().color = color; Color textColor = color.IdealTextColor(); buttonManager.normalText.color = textColor; - buttonManager.normalImage.color = textColor; + iconText.color = textColor; } /// diff --git a/Assets/SEE/UI/Menu/MenuEntry.cs b/Assets/SEE/UI/Menu/MenuEntry.cs index 1070bf7dea..4690a82fbd 100644 --- a/Assets/SEE/UI/Menu/MenuEntry.cs +++ b/Assets/SEE/UI/Menu/MenuEntry.cs @@ -15,8 +15,10 @@ namespace SEE.UI.Menu /// A description of the entry. /// The color with which this entry shall be displayed. /// Whether this entry should be enabled (i.e., whether it can be selected). - /// The icon which shall be displayed alongside this entry. - public record MenuEntry(Action SelectAction, string Title, Action UnselectAction = null, string Description = null, Color EntryColor = default, bool Enabled = true, Sprite Icon = null) + /// The icon which shall be displayed alongside this entry, + /// given as a FontAwesome codepoint. See for more information. + public record MenuEntry(Action SelectAction, string Title, Action UnselectAction = null, string Description = null, + Color EntryColor = default, bool Enabled = true, char Icon = ' ') { /// /// The color of this entry when disabled. diff --git a/Assets/SEE/UI/Menu/NestedMenu.cs b/Assets/SEE/UI/Menu/NestedMenu.cs index 1ee772b8f5..bb253abc20 100644 --- a/Assets/SEE/UI/Menu/NestedMenu.cs +++ b/Assets/SEE/UI/Menu/NestedMenu.cs @@ -132,7 +132,7 @@ private void DescendLevel(NestedMenuEntry nestedEntry, bool withBreadcrumb = // as the title is technically the last element in the breadcrumb) string breadcrumb = withBreadcrumb ? GetBreadcrumb() : string.Empty; Description = nestedEntry.Description + (breadcrumb.Length > 0 ? $"\n{GetBreadcrumb()}" : ""); - Icon = nestedEntry.Icon; + Icon = nestedEntry.MenuIconSprite; nestedEntry.InnerEntries.ForEach(AddEntry); KeywordListener.Unregister(HandleKeyword); KeywordListener.Register(HandleKeyword); @@ -311,8 +311,7 @@ private async UniTaskVoid SearchTextEnteredAsync() .Select(x => allEntries[x.Value]) .ToList(); - NestedMenuEntry resultEntry = new(results, Title, Description, - default, default, Icon); + NestedMenuEntry resultEntry = new(results, Title, Description, menuIconSprite: Icon); DescendLevel(resultEntry, withBreadcrumb: false); } finally diff --git a/Assets/SEE/UI/Menu/NestedMenuEntry.cs b/Assets/SEE/UI/Menu/NestedMenuEntry.cs index 0f047144ad..ce96157c5f 100644 --- a/Assets/SEE/UI/Menu/NestedMenuEntry.cs +++ b/Assets/SEE/UI/Menu/NestedMenuEntry.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using SEE.Utils; using UnityEngine; namespace SEE.UI.Menu @@ -16,6 +17,11 @@ public record NestedMenuEntry : MenuEntry where T : MenuEntry /// public readonly List InnerEntries; + /// + /// The sprite of the icon of the menu itself. + /// + public readonly Sprite MenuIconSprite; + /// /// Instantiates and returns a new . /// @@ -24,12 +30,16 @@ public record NestedMenuEntry : MenuEntry where T : MenuEntry /// A description of the entry. /// The color with which this entry shall be displayed. /// Whether this entry should be enabled on creation. - /// The icon which shall be displayed alongside this entry. + /// The FontAwesome icon which shall be displayed alongside this entry. + /// The sprite of the icon of the menu itself. /// If is null. - public NestedMenuEntry(IEnumerable innerEntries, string title, string description = null, Color entryColor = default, bool enabled = true, Sprite icon = null) : - base(() => { }, title, () => { }, description, entryColor, enabled, icon) + public NestedMenuEntry(IEnumerable innerEntries, string title, string description = null, + Color entryColor = default, bool enabled = true, + char icon = Icons.Bars, Sprite menuIconSprite = null) + : base(() => { }, title, () => { }, description, entryColor, enabled, icon) { InnerEntries = innerEntries?.ToList() ?? throw new ArgumentNullException(nameof(innerEntries)); + MenuIconSprite = menuIconSprite; } } } diff --git a/Assets/SEE/UI/OpeningDialog.cs b/Assets/SEE/UI/OpeningDialog.cs index aca2444294..1fb481eabc 100644 --- a/Assets/SEE/UI/OpeningDialog.cs +++ b/Assets/SEE/UI/OpeningDialog.cs @@ -57,26 +57,26 @@ private IList SelectionEntries() Title: "Host", Description: "Starts a server and local client process.", EntryColor: NextColor(), - Icon: Resources.Load("Icons/Host")), + Icon: Icons.Broadcast), new(SelectAction: StartClient, Title: "Client", Description: "Starts a local client connection to a server.", EntryColor: NextColor(), - Icon: Resources.Load("Icons/Client")), + Icon: Icons.Link), #if ENABLE_VR new(SelectAction: ToggleEnvironment, Title: "Toggle Desktop/VR", Description: "Toggles between desktop and VR hardware.", EntryColor: NextColor(), - Icon: Resources.Load("Icons/Client")), + Icon: Icons.VR), #endif new(SelectAction: Settings, Title: "Settings", Description: "Allows to set additional network settings.", EntryColor: Color.gray, - Icon: Resources.Load("Icons/Settings")), + Icon: Icons.Gear) }; Color NextColor() diff --git a/Assets/SEE/UI/PropertyDialog/ButtonProperty.cs b/Assets/SEE/UI/PropertyDialog/ButtonProperty.cs index 96b77785ab..374fc2bb4d 100644 --- a/Assets/SEE/UI/PropertyDialog/ButtonProperty.cs +++ b/Assets/SEE/UI/PropertyDialog/ButtonProperty.cs @@ -10,7 +10,7 @@ namespace SEE.UI.PropertyDialog { /// - /// A button for a for a property dialog. + /// A button for a property dialog. /// public class ButtonProperty : Property { @@ -30,9 +30,9 @@ public class ButtonProperty : Property private GameObject button; /// - /// Used to store the icon of the button. + /// The codepoint of the icon for the button. /// - public Sprite IconSprite; + public char Icon; /// /// Saves which method of the hide action is to be executed. @@ -80,6 +80,7 @@ protected override void StartDesktop() SetupTooltip(); SetUpButton(); + return; void SetUpButton() { @@ -87,10 +88,10 @@ void SetUpButton() GameObject text = button.transform.Find("Text").gameObject; GameObject icon = button.transform.Find("Icon").gameObject; - if (!button.TryGetComponentOrLog(out ButtonManagerBasicWithIcon buttonManager) + if (!button.TryGetComponentOrLog(out ButtonManagerBasic buttonManager) || !button.TryGetComponentOrLog(out Image buttonImage) || !text.TryGetComponentOrLog(out TextMeshProUGUI textMeshPro) - || !icon.TryGetComponentOrLog(out Image iconImage) + || !icon.TryGetComponentOrLog(out TextMeshProUGUI iconText) || !button.TryGetComponentOrLog(out PointerHelper pointerHelper)) { return; @@ -99,8 +100,8 @@ void SetUpButton() textMeshPro.fontSize = 20; buttonImage.color = ButtonColor; textMeshPro.color = ButtonColor.IdealTextColor(); - iconImage.color = ButtonColor.IdealTextColor(); - iconImage.sprite = IconSprite; + iconText.color = ButtonColor.IdealTextColor(); + iconText.text = Icon.ToString(); buttonManager.buttonText = Name; buttonManager.clickEvent.AddListener(Clicked); diff --git a/Assets/SEE/UI/RuntimeConfigMenu/RuntimeTabMenu.cs b/Assets/SEE/UI/RuntimeConfigMenu/RuntimeTabMenu.cs index 643b816d4a..91bb346246 100644 --- a/Assets/SEE/UI/RuntimeConfigMenu/RuntimeTabMenu.cs +++ b/Assets/SEE/UI/RuntimeConfigMenu/RuntimeTabMenu.cs @@ -392,7 +392,7 @@ private GameObject CreateOrGetViewGameObject(IEnumerable attributes) Title: tabName, Description: $"Settings for {tabName}", EntryColor: GetColorForTab(), - Icon: Resources.Load("Materials/Charts/MoveIcon") + Icon: Icons.Move ); AddEntry(entry); } diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs b/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs index 0068e6869f..f8fa3d7ce0 100644 --- a/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs @@ -232,7 +232,7 @@ private async UniTask> MenuEntriesForLocationsAsync(IUniTaskAsy } entries.Add(new(SelectAction: () => OpenSelection(targetUri, targetRange), Title: title ?? $"{uri}: {targetRange}", - // TODO: Icon + Icon: Icons.Crosshairs, EntryColor: new Color(0.051f, 0.3608f, 0.1333f))); } await UniTask.SwitchToMainThread(); diff --git a/Assets/SEE/Utils/Icons.cs b/Assets/SEE/Utils/Icons.cs index 370a6abc72..1c3924ef30 100644 --- a/Assets/SEE/Utils/Icons.cs +++ b/Assets/SEE/Utils/Icons.cs @@ -10,39 +10,55 @@ namespace SEE.Utils /// public static class Icons { - public const char Node = '\uF1B2'; - public const char Edge = '\uF542'; - public const char OutgoingEdge = '\uF2F5'; - public const char IncomingEdge = '\uF2F6'; - public const char LiftedIncomingEdge = '\uF090'; - public const char LiftedOutgoingEdge = '\uF08B'; - public const char EmptyCheckbox = '\uF0C8'; + public const char ArrowRotateLeft = '\uF0E2'; + public const char Bars = '\uF0C9'; + public const char Broadcast = '\uF519'; + public const char Chalkboard = '\uF51B'; public const char CheckedCheckbox = '\uF14A'; - public const char MinusCheckbox = '\uF146'; + public const char CheckedRadio = '\uF192'; public const char Checkmark = '\uF00C'; - public const char Trash = '\uF1F8'; - public const char Info = '\uF05A'; - public const char LightBulb = '\uF0EB'; + public const char CircleCheckmark = '\uF058'; + public const char CircleExclamationMark = '\uF06A'; + public const char CircleMinus = '\uF056'; + public const char CircleQuestionMark = '\uF059'; + public const char Crosshairs = '\uF05B'; public const char Code = '\uF121'; - public const char TreeView = '\uF802'; public const char Compare = '\uE13A'; + public const char Edge = '\uF542'; + public const char EmptyCheckbox = '\uF0C8'; + public const char EmptyRadio = '\uF111'; + public const char Export = '\uF56E'; + public const char Eye = '\uF06E'; + public const char EyeSlash = '\uF070'; + public const char Gear = '\uF013'; + public const char Hashtag = '#'; public const char Hide = '\uF070'; - public const char Show = '\uF06E'; + public const char Import = '\uF56F'; + public const char IncomingEdge = '\uF2F6'; + public const char Info = '\uF05A'; + public const char LiftedIncomingEdge = '\uF090'; + public const char LiftedOutgoingEdge = '\uF08B'; + public const char LightBulb = '\uF0EB'; + public const char Link = '\uF0C1'; + public const char MagnifyingGlass = '\uF002'; + public const char MinusCheckbox = '\uF146'; + public const char Move = '\uF0B2'; + public const char Node = '\uF1B2'; + public const char OutgoingEdge = '\uF2F5'; + public const char PenToSquare = '\uF044'; + public const char Pencil = '\uF303'; public const char QuestionMark = '?'; - public const char ArrowRotateLeft = '\uF0E2'; - public const char SortAlphabeticalUp = '\uF15E'; + public const char Rotate = '\uF2F1'; + public const char Scale = '\uF424'; + public const char Show = '\uF06E'; + public const char Sitemap = '\uF0E8'; public const char SortAlphabeticalDown = '\uF15D'; - public const char SortNumericUp = '\uF163'; + public const char SortAlphabeticalUp = '\uF15E'; public const char SortNumericDown = '\uF162'; - public const char Hashtag = '#'; + public const char SortNumericUp = '\uF163'; public const char Text = '\uF031'; - public const char EmptyRadio = '\uF111'; - public const char CheckedRadio = '\uF192'; - public const char CircleMinus = '\uF056'; - public const char CircleCheckmark = '\uF058'; - public const char CircleQuestionMark = '\uF059'; - public const char CircleExclamationMark = '\uF06A'; - public const char Sitemap = '\uF0E8'; - public const char MagnifyingGlass = '\uF002'; + public const char Trash = '\uF1F8'; + public const char TreeView = '\uF802'; + public const char VR = '\uF729'; } } diff --git a/Assets/SEEPlayModeTests/TestMenu.cs b/Assets/SEEPlayModeTests/TestMenu.cs index 4a198d529a..514fca50c2 100644 --- a/Assets/SEEPlayModeTests/TestMenu.cs +++ b/Assets/SEEPlayModeTests/TestMenu.cs @@ -73,18 +73,23 @@ internal abstract class TestMenu : TestUI /// protected const float TimeUntilMenuIsSetup = 1f; + /// + /// An example icon. + /// + public const char ExampleIcon = Icons.Move; + /// /// Path to a sprite we can use for testing. /// - private const string PathOfIcon = "Materials/Charts/MoveIcon"; + private const string PathOfIconSprite = "Materials/Charts/MoveIcon"; /// - /// The icon loaded from . + /// The icon loaded from . /// - /// icon loaded from - protected static Sprite GetIcon() + /// icon loaded from + protected static Sprite GetIconSprite() { - return Resources.Load(PathOfIcon); + return Resources.Load(PathOfIconSprite); } /// @@ -122,4 +127,4 @@ protected static void PressCloseButton(string menuTitle) PressButton($"/UI Canvas/{menuTitle}/Main Content/Buttons/Content/Close"); } } -} \ No newline at end of file +} diff --git a/Assets/SEEPlayModeTests/TestNestedMenu.cs b/Assets/SEEPlayModeTests/TestNestedMenu.cs index 146deb08a6..018cb8fda4 100644 --- a/Assets/SEEPlayModeTests/TestNestedMenu.cs +++ b/Assets/SEEPlayModeTests/TestNestedMenu.cs @@ -126,7 +126,7 @@ protected override void CreateMenu(out GameObject menuGO, out SimpleListMenu menuEntries = new List { @@ -134,24 +134,24 @@ protected override void CreateMenu(out GameObject menuGO, out SimpleListMenu(innerEntries: new List { new(SelectAction: () => selection = NestedOptionOneValue, Title: NestedOptionOne, Description: "Select option 2a", EntryColor: Color.green, - Icon: GetIcon()), + Icon: ExampleIcon), new(SelectAction: () => selection = NestedOptionTwoValue, Title: NestedOptionTwo, Description: "Select option 2b", EntryColor: Color.green, - Icon: GetIcon()) + Icon: ExampleIcon) }, title: SubMenuTitle, description: "open subselection 2", entryColor: Color.red, - icon: GetIcon()) + icon: ExampleIcon) }; menu.AddEntries(menuEntries); diff --git a/Assets/SEEPlayModeTests/TestSimpleMenu.cs b/Assets/SEEPlayModeTests/TestSimpleMenu.cs index a630925f31..0a6d114450 100644 --- a/Assets/SEEPlayModeTests/TestSimpleMenu.cs +++ b/Assets/SEEPlayModeTests/TestSimpleMenu.cs @@ -84,7 +84,7 @@ protected override void CreateMenu(out GameObject menuGO, out SimpleListMenu menuEntries = new List { @@ -92,12 +92,12 @@ protected override void CreateMenu(out GameObject menuGO, out SimpleListMenu selection = 2, Title: OptionTwo, Description: "Select option 2", EntryColor: Color.green, - Icon: GetIcon()), + Icon: ExampleIcon), }; menu.AddEntries(menuEntries); diff --git a/Assets/SEETests/TestActionHistory.cs b/Assets/SEETests/TestActionHistory.cs index 6cb732a94e..635165548b 100644 --- a/Assets/SEETests/TestActionHistory.cs +++ b/Assets/SEETests/TestActionHistory.cs @@ -8,7 +8,7 @@ namespace SEE.Utils.History /// /// Test cases for . /// - class TestActionHistory + internal class TestActionHistory { /// /// Shortcut to a constructor of that requires @@ -23,7 +23,7 @@ private abstract class TestActionStateType : ActionStateType /// value for protected TestActionStateType(string name, CreateReversibleAction createReversible) : base(name: name, description: "", color: UnityEngine.Color.white, - iconPath: "", createReversible: createReversible, register: false) + icon: ' ', createReversible: createReversible, register: false) { } } diff --git a/Assets/SEETests/TestActionStateType.cs b/Assets/SEETests/TestActionStateType.cs index 9d9d2a9295..638d535bcb 100644 --- a/Assets/SEETests/TestActionStateType.cs +++ b/Assets/SEETests/TestActionStateType.cs @@ -56,8 +56,8 @@ public void ActionStateTypesAllRootTypesJustContainsAllRoots() [Test] public void TestNoAttributeNull() { - Assert.IsEmpty(allRootTypes.AllElements().Where(x => x.Description == null || x.Name == null || x.IconPath == null), - "No attribute of an AbstractActionStateType may be null!"); + Assert.IsEmpty(allRootTypes.AllElements().Where(x => x.Description == null || x.Name == null || x.Icon == default), + "No attribute of an AbstractActionStateType may be null or default!"); } [Test] @@ -81,4 +81,4 @@ public void TestEquality(AbstractActionStateType type) "An ActionStateType must only be equal to itself!"); } } -} \ No newline at end of file +} diff --git a/Assets/SEETests/UI/TestMenuEntry.cs b/Assets/SEETests/UI/TestMenuEntry.cs index 51ac42b389..081489891f 100644 --- a/Assets/SEETests/UI/TestMenuEntry.cs +++ b/Assets/SEETests/UI/TestMenuEntry.cs @@ -12,26 +12,25 @@ namespace SEE.UI.Menu internal class TestMenuEntry { /// - /// Path to a sprite we can use for testing. + /// An icon used for testing. /// - private const string TEST_SPRITE = "Materials/Charts/MoveIcon"; + private const char testIcon = '!'; protected static IEnumerable ValidConstructorSupplier() { - Sprite testSprite = Resources.Load(TEST_SPRITE); - yield return new TestCaseData(new UnityAction(() => { }), "Test", "Test description", Color.red, - true, testSprite); + yield return new TestCaseData(new Action(() => { }), "Test", "Test description", Color.red, + true, testIcon); yield return new TestCaseData(null, "Test", "Test description", Color.green, - true, testSprite); - yield return new TestCaseData(new UnityAction(() => { }), "Test", null, Color.blue, - true, testSprite); - yield return new TestCaseData(new UnityAction(() => { }), "Test", "Test description", null, - true, testSprite); - yield return new TestCaseData(new UnityAction(() => { }), "Test", "Test description", Color.white, - false, testSprite); - yield return new TestCaseData(new UnityAction(() => { }), "Test", "Test description", Color.black, - true, null); - yield return new TestCaseData(null, "Test", null, null, true, null); + true, testIcon); + yield return new TestCaseData(new Action(() => { }), "Test", null, Color.blue, + true, testIcon); + yield return new TestCaseData(new Action(() => { }), "Test", "Test description", null, + true, testIcon); + yield return new TestCaseData(new Action(() => { }), "Test", "Test description", Color.white, + false, testIcon); + yield return new TestCaseData(new Action(() => { }), "Test", "Test description", Color.black, + true, ' '); + yield return new TestCaseData(null, "Test", null, null, true, ' '); } /// @@ -39,18 +38,11 @@ protected static IEnumerable ValidConstructorSupplier() /// /// The newly constructed MenuEntry. protected virtual MenuEntry CreateMenuEntry(Action action, string title, string description = null, - Color entryColor = default, bool enabled = true, Sprite icon = null) + Color entryColor = default, bool enabled = true, char icon = ' ') { return new MenuEntry(action, title, null, description, entryColor, enabled, icon); } - [Test] - public void TestConstructorTitleNull() - { - Assert.Throws(() => _ = CreateMenuEntry(null, null)); - Assert.Throws(() => _ = CreateMenuEntry(() => { }, null)); - } - [Test] public void TestConstructorDefault() { @@ -60,7 +52,7 @@ public void TestConstructorDefault() Assert.AreEqual(null, entry.Description); Assert.AreEqual("Test", entry.Title); Assert.AreEqual(true, entry.Enabled); - Assert.AreEqual(null, entry.Icon); + Assert.AreEqual(' ', entry.Icon); Assert.AreEqual(default(Color), entry.EntryColor); #if INCLUDE_STEAM_VR @@ -73,7 +65,7 @@ public void TestConstructorDefault() [Test, TestCaseSource(nameof(ValidConstructorSupplier))] public void TestConstructor(Action action, string title, string description, - Color entryColor, bool enabled, Sprite icon) + Color entryColor, bool enabled, char icon) { MenuEntry entry = CreateMenuEntry(action, title, description, entryColor, enabled, icon); // Given action must either be null or NOP for this test diff --git a/Assets/SEETests/UI/TestToggleMenuEntry.cs b/Assets/SEETests/UI/TestToggleMenuEntry.cs index 660145bada..1eaa91654c 100644 --- a/Assets/SEETests/UI/TestToggleMenuEntry.cs +++ b/Assets/SEETests/UI/TestToggleMenuEntry.cs @@ -15,7 +15,7 @@ internal class TestToggleMenuEntry: TestMenuEntry { protected override MenuEntry CreateMenuEntry(Action action, string title, string description = null, Color entryColor = default, bool enabled = true, - Sprite icon = null) + char icon = '#') { return new MenuEntry(action, title, null, description, entryColor, enabled, icon); } From c8da4f63bd96b7351828d3d3f87b92bb2411c6cb Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Sat, 27 Jul 2024 20:37:59 +0200 Subject: [PATCH 18/23] Apply automated review comment suggestions --- Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs | 1 - Assets/SEE/Utils/Markdown/RichTagsMarkdownBlockRenderers.cs | 2 +- Assets/SEE/Utils/Markdown/RichTagsMarkdownInlineRenderers.cs | 2 +- Assets/SEE/Utils/Markdown/RichTagsMarkdownRenderer.cs | 2 +- Assets/SEE/Utils/StringExtensions.cs | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs b/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs index 13650c83be..888fb55c91 100644 --- a/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs +++ b/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs @@ -313,7 +313,6 @@ private async UniTaskVoid TriggerLspHoverAsync(TMP_WordInfo? hoveredWord) { Assert.IsNotNull(lspHandler); - // TODO: Handle conflicts between LSP Hover and Debug Hover / Issue Hover if (hoveredWord == null) { Tooltip.Deactivate(); diff --git a/Assets/SEE/Utils/Markdown/RichTagsMarkdownBlockRenderers.cs b/Assets/SEE/Utils/Markdown/RichTagsMarkdownBlockRenderers.cs index 51590ead90..00f70daf4b 100644 --- a/Assets/SEE/Utils/Markdown/RichTagsMarkdownBlockRenderers.cs +++ b/Assets/SEE/Utils/Markdown/RichTagsMarkdownBlockRenderers.cs @@ -89,4 +89,4 @@ protected override void Write(RichTagsMarkdownRenderer renderer, ParagraphBlock } } } -} \ No newline at end of file +} diff --git a/Assets/SEE/Utils/Markdown/RichTagsMarkdownInlineRenderers.cs b/Assets/SEE/Utils/Markdown/RichTagsMarkdownInlineRenderers.cs index fecfe01a20..a1adb17be2 100644 --- a/Assets/SEE/Utils/Markdown/RichTagsMarkdownInlineRenderers.cs +++ b/Assets/SEE/Utils/Markdown/RichTagsMarkdownInlineRenderers.cs @@ -88,4 +88,4 @@ protected override void Write(RichTagsMarkdownRenderer renderer, LiteralInline o } } } -} \ No newline at end of file +} diff --git a/Assets/SEE/Utils/Markdown/RichTagsMarkdownRenderer.cs b/Assets/SEE/Utils/Markdown/RichTagsMarkdownRenderer.cs index 637998aba1..c358ce9a4e 100644 --- a/Assets/SEE/Utils/Markdown/RichTagsMarkdownRenderer.cs +++ b/Assets/SEE/Utils/Markdown/RichTagsMarkdownRenderer.cs @@ -80,4 +80,4 @@ private abstract class RichTagsObjectRenderer : MarkdownObjectRenderer< { } } -} \ No newline at end of file +} diff --git a/Assets/SEE/Utils/StringExtensions.cs b/Assets/SEE/Utils/StringExtensions.cs index 1b2c1fd336..518c59383e 100644 --- a/Assets/SEE/Utils/StringExtensions.cs +++ b/Assets/SEE/Utils/StringExtensions.cs @@ -97,4 +97,4 @@ public static string WithoutRichTextTags(this string input) return builder.ToString(); } } -} \ No newline at end of file +} From ad190b389740dbc1b1905ff435261fd9e2a79e23 Mon Sep 17 00:00:00 2001 From: Rainer Koschke Date: Fri, 2 Aug 2024 13:48:14 +0200 Subject: [PATCH 19/23] Added references to the interface methods implemented. --- Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs | 6 ++++++ Assets/SEE/Tools/LSP/LSPIssue.cs | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs b/Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs index a8f93690b0..75c6eb6349 100644 --- a/Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs +++ b/Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs @@ -224,8 +224,14 @@ public IssueComment(string username, string userDisplayName, DateTime date, $"" }; + /// + /// Implements . + /// public string Source => "Axivion Dashboard"; + /// + /// Implements . + /// public IEnumerable<(string Path, Range Range)> Occurrences => Entities.Select(e => (e.Path, new Range(e.Line, (e.EndLine ?? e.Line) + 1))); public (int startCharacter, int endCharacter)? GetCharacterRangeForLine(string path, int lineNumber, string line) diff --git a/Assets/SEE/Tools/LSP/LSPIssue.cs b/Assets/SEE/Tools/LSP/LSPIssue.cs index 73e9bcd2d1..abec60f12e 100644 --- a/Assets/SEE/Tools/LSP/LSPIssue.cs +++ b/Assets/SEE/Tools/LSP/LSPIssue.cs @@ -15,6 +15,9 @@ namespace SEE.Tools.LSP /// The diagnostic that represents the issue. public record LSPIssue(string Path, Diagnostic Diagnostic) : IDisplayableIssue { + /// + /// Implements . + /// public UniTask ToDisplayStringAsync() { string message = ""; @@ -26,8 +29,14 @@ public UniTask ToDisplayStringAsync() return UniTask.FromResult(message); } + /// + /// Implements . + /// public string Source => Diagnostic.Source ?? "LSP"; + /// + /// Implements . + /// public IList RichTags { get @@ -76,6 +85,9 @@ private static string DiagnosticSeverityToTag(DiagnosticSeverity severity) => _ => throw new ArgumentOutOfRangeException(nameof(severity), severity, "Unknown diagnostic severity") }; + /// + /// Implements . + /// public IEnumerable<(string Path, Range Range)> Occurrences { get From 42af37c86d4256953d919ffba8871e090d744cd0 Mon Sep 17 00:00:00 2001 From: Rainer Koschke Date: Fri, 2 Aug 2024 13:48:26 +0200 Subject: [PATCH 20/23] Fixed comment. --- Assets/SEE/UI/RuntimeConfigMenu/RuntimeTabMenu.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/SEE/UI/RuntimeConfigMenu/RuntimeTabMenu.cs b/Assets/SEE/UI/RuntimeConfigMenu/RuntimeTabMenu.cs index 91bb346246..a6737ad38f 100644 --- a/Assets/SEE/UI/RuntimeConfigMenu/RuntimeTabMenu.cs +++ b/Assets/SEE/UI/RuntimeConfigMenu/RuntimeTabMenu.cs @@ -263,7 +263,7 @@ string GetButtonGroup(MemberInfo memberInfo) => (memberInfo.GetCustomAttributes().OfType().FirstOrDefault() ?? new RuntimeButtonAttribute(null, null)).Name; - // ordered depending on if a setting is primitive or has nested settings + // ordered depending on whether a setting is primitive or has nested settings bool SortIsNotNested(MemberInfo memberInfo) { object value; From 272de5eb5f6366ac31d9e98f6b7dcd7cf58dc48d Mon Sep 17 00:00:00 2001 From: Rainer Koschke Date: Fri, 2 Aug 2024 13:49:43 +0200 Subject: [PATCH 21/23] Upgraded to Unity version 2022.3.40f1. --- Axivion/axivion-jenkins.bat | 2 +- ProjectSettings/ProjectVersion.txt | 4 ++-- README.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Axivion/axivion-jenkins.bat b/Axivion/axivion-jenkins.bat index 7fe5373d96..7b6845125f 100644 --- a/Axivion/axivion-jenkins.bat +++ b/Axivion/axivion-jenkins.bat @@ -107,7 +107,7 @@ if "%AXIVION_DASHBOARD_URL%"=="" ( ) if "%UNITY%"=="" ( - set "UNITY=C:\Program Files\Unity\Hub\Editor\2022.3.38f1" + set "UNITY=C:\Program Files\Unity\Hub\Editor\2022.3.40f1" ) if not exist "%UNITY%" ( diff --git a/ProjectSettings/ProjectVersion.txt b/ProjectSettings/ProjectVersion.txt index 793a36e15f..cfbed01351 100644 --- a/ProjectSettings/ProjectVersion.txt +++ b/ProjectSettings/ProjectVersion.txt @@ -1,2 +1,2 @@ -m_EditorVersion: 2022.3.38f1 -m_EditorVersionWithRevision: 2022.3.38f1 (c5d5a7410213) +m_EditorVersion: 2022.3.40f1 +m_EditorVersionWithRevision: 2022.3.40f1 (cbdda657d2f0) diff --git a/README.md b/README.md index 2ee8b9a1a3..71d7dcdbe7 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Tests](https://github.com/uni-bremen-agst/SEE/actions/workflows/main.yml/badge.svg)](https://github.com/uni-bremen-agst/SEE/actions/workflows/main.yml) SEE visualizes hierarchical dependency graphs of software in 3D/VR based on the city metaphor. -The underlying game engine is Unity 3D (version 2022.3.38f1). +The underlying game engine is Unity 3D (version 2022.3.40f1). ![Screenshot of SEE](Screenshot.png) From 6d365494411af999210e38fc56b589220efda41f Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Fri, 2 Aug 2024 14:15:00 +0200 Subject: [PATCH 22/23] Address review comments by @koschke for #751 --- .../CodeWindow/CodeWindowContextMenu.cs | 4 ++++ .../SEE/Utils/Markdown/MarkdownConverter.cs | 3 +++ .../RichTagsMarkdownBlockRenderers.cs | 3 +++ .../RichTagsMarkdownInlineRenderers.cs | 3 +++ Assets/SEE/Utils/ReferenceEqualityComparer.cs | 24 +++++++++++++++++++ 5 files changed, 37 insertions(+) diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs b/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs index f8fa3d7ce0..c48e49a9ce 100644 --- a/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs @@ -17,6 +17,10 @@ namespace SEE.UI.Window.CodeWindow { + /// + /// Partial class containing methods related to context menus and LSP navigation in code windows. + /// In addition, this part contains a record representing a context menu handler for a code window. + /// public partial class CodeWindow { /// diff --git a/Assets/SEE/Utils/Markdown/MarkdownConverter.cs b/Assets/SEE/Utils/Markdown/MarkdownConverter.cs index db4744eef3..449611cb10 100644 --- a/Assets/SEE/Utils/Markdown/MarkdownConverter.cs +++ b/Assets/SEE/Utils/Markdown/MarkdownConverter.cs @@ -7,6 +7,9 @@ namespace SEE.Utils.Markdown { + /// + /// Utility class for converting markdown text to TextMeshPro-compatible rich text. + /// public static class MarkdownConverter { /// diff --git a/Assets/SEE/Utils/Markdown/RichTagsMarkdownBlockRenderers.cs b/Assets/SEE/Utils/Markdown/RichTagsMarkdownBlockRenderers.cs index 00f70daf4b..7dc56fa9ea 100644 --- a/Assets/SEE/Utils/Markdown/RichTagsMarkdownBlockRenderers.cs +++ b/Assets/SEE/Utils/Markdown/RichTagsMarkdownBlockRenderers.cs @@ -3,6 +3,9 @@ namespace SEE.Utils.Markdown { + /// + /// Partial class that contains renderers for block Markdown elements. + /// public partial class RichTagsMarkdownRenderer { /// diff --git a/Assets/SEE/Utils/Markdown/RichTagsMarkdownInlineRenderers.cs b/Assets/SEE/Utils/Markdown/RichTagsMarkdownInlineRenderers.cs index a1adb17be2..14c4c6bbb8 100644 --- a/Assets/SEE/Utils/Markdown/RichTagsMarkdownInlineRenderers.cs +++ b/Assets/SEE/Utils/Markdown/RichTagsMarkdownInlineRenderers.cs @@ -2,6 +2,9 @@ namespace SEE.Utils.Markdown { + /// + /// Partial class that contains renderers for inline Markdown elements. + /// public partial class RichTagsMarkdownRenderer { /// diff --git a/Assets/SEE/Utils/ReferenceEqualityComparer.cs b/Assets/SEE/Utils/ReferenceEqualityComparer.cs index 440c9f6a79..9eb178f33a 100644 --- a/Assets/SEE/Utils/ReferenceEqualityComparer.cs +++ b/Assets/SEE/Utils/ReferenceEqualityComparer.cs @@ -6,6 +6,30 @@ namespace SEE.Utils // NOTE: The below class was copied from the .NET 5.x source code. // Unity uses .NET 4.x, so this class would otherwise not be available. + // Original license for this code: + // The MIT License (MIT) + // Copyright (c) .NET Foundation and Contributors + // + // All rights reserved. + // + // Permission is hereby granted, free of charge, to any person obtaining a copy + // of this software and associated documentation files (the "Software"), to deal + // in the Software without restriction, including without limitation the rights + // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + // copies of the Software, and to permit persons to whom the Software is + // furnished to do so, subject to the following conditions: + // + // The above copyright notice and this permission notice shall be included in all + // copies or substantial portions of the Software. + // + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + // SOFTWARE. + /// /// An that uses reference equality () /// instead of value equality () when comparing two object instances. From 1d1099a091cac5a89ca8a854d1b6d037b457b180 Mon Sep 17 00:00:00 2001 From: Rainer Koschke Date: Fri, 2 Aug 2024 15:23:59 +0200 Subject: [PATCH 23/23] Added reference to the interface method implemented. --- Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs b/Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs index 75c6eb6349..959eb1fa63 100644 --- a/Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs +++ b/Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs @@ -234,6 +234,9 @@ public IssueComment(string username, string userDisplayName, DateTime date, /// public IEnumerable<(string Path, Range Range)> Occurrences => Entities.Select(e => (e.Path, new Range(e.Line, (e.EndLine ?? e.Line) + 1))); + /// + /// Implements . + /// public (int startCharacter, int endCharacter)? GetCharacterRangeForLine(string path, int lineNumber, string line) { // Axivion Dashboard doesn't provide character ranges for issues, so we have to calculate them ourselves.