diff --git a/Debuggee/Debuggee.projitems b/Debuggee/Debuggee.projitems new file mode 100644 index 0000000..97742df --- /dev/null +++ b/Debuggee/Debuggee.projitems @@ -0,0 +1,14 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + cc83f717-2d1d-4b45-bead-be7fa88bb71c + + + Debuggee + + + + + \ No newline at end of file diff --git a/Debuggee/Debuggee.shproj b/Debuggee/Debuggee.shproj new file mode 100644 index 0000000..da2f7fb --- /dev/null +++ b/Debuggee/Debuggee.shproj @@ -0,0 +1,13 @@ + + + + cc83f717-2d1d-4b45-bead-be7fa88bb71c + 14.0 + + + + + + + + diff --git a/Debuggee/VisualizerDataObjectSource.cs b/Debuggee/VisualizerDataObjectSource.cs new file mode 100644 index 0000000..12be5d9 --- /dev/null +++ b/Debuggee/VisualizerDataObjectSource.cs @@ -0,0 +1,10 @@ +using System.Reflection; +using ParseTreeVisualizer.Serialization; +using Periscope.Debuggee; + +namespace ParseTreeVisualizer { + public class VisualizerDataObjectSource : VisualizerObjectSourceBase { + public override object GetResponse(object target, Config config) => new VisualizerData(target, config); + public override string GetConfigKey(object target) => Assembly.GetAssembly(target.GetType()).GetName().Name; + } +} diff --git a/Runtime.Debugee/Periscope.Debuggee b/Runtime.Debuggee/Periscope.Debuggee similarity index 100% rename from Runtime.Debugee/Periscope.Debuggee rename to Runtime.Debuggee/Periscope.Debuggee diff --git a/Runtime.Debuggee/Runtime.Debuggee.csproj b/Runtime.Debuggee/Runtime.Debuggee.csproj new file mode 100644 index 0000000..7515c8a --- /dev/null +++ b/Runtime.Debuggee/Runtime.Debuggee.csproj @@ -0,0 +1,30 @@ + + + + net452 + ParseTreeVisualizer.Runtime.Debuggee + ParseTreeVisualizer.Runtime.Debuggee + 9.0 + enable + VISUALIZER_DEBUGGEE, ANTLR_RUNTIME + + + + false + false + bin/$(Configuration)/net2.0/ + + + + + + + C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\PublicAssemblies\Microsoft.VisualStudio.DebuggerVisualizers.dll + false + + + + + + + diff --git a/RuntimeStandard.Debuggee/Class1.cs b/RuntimeStandard.Debuggee/Class1.cs new file mode 100644 index 0000000..081414c --- /dev/null +++ b/RuntimeStandard.Debuggee/Class1.cs @@ -0,0 +1,6 @@ +using System; + +namespace RuntimeStandard.Debuggee { + public class Class1 { + } +} diff --git a/RuntimeStandard.Debuggee/RuntimeStandard.Debuggee.csproj b/RuntimeStandard.Debuggee/RuntimeStandard.Debuggee.csproj new file mode 100644 index 0000000..9f5c4f4 --- /dev/null +++ b/RuntimeStandard.Debuggee/RuntimeStandard.Debuggee.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.0 + + + diff --git a/Serialization/ClassInfo.cs b/Serialization/ClassInfo.cs new file mode 100644 index 0000000..628a11b --- /dev/null +++ b/Serialization/ClassInfo.cs @@ -0,0 +1,54 @@ +using Antlr4.Runtime; +using Antlr4.Runtime.Tree; +using System; +using System.CodeDom.Compiler; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; +using ZSpitz.Util; + +namespace ParseTreeVisualizer.Serialization { + [Serializable] + public class ClassInfo { + public static readonly ClassInfo None = new("(None)"); + + public string Name { get; } + public string? Namespace { get; } + public string? Assembly { get; } + public string? Antlr { get; private set; } // Runtime - in Antlr.Runtime, - version + public string? FullName { get; } + public string? RuleName { get; } + public int? RuleID { get; } + + public ReadOnlyCollection? MethodNames { get; } + + private ClassInfo(string name) => Name = name; + public ClassInfo(Type t, string? ruleName = null, int? ruleID = null, bool loadMethodNames = false) { + if (t is null) { throw new ArgumentNullException(nameof(t)); } + + Name = t.Name; + Namespace = t.Namespace; + Assembly = t.Assembly.Location; + if (t.Assembly == typeof(IParseTree).Assembly) { + Antlr = "Runtime"; + } else if (t.GetCustomAttribute() is var attr) { + Antlr = attr.Version; + } + FullName = t.FullName; + RuleName = ruleName.IsNullOrWhitespace() ? null : ruleName; + RuleID = ruleID; + + if (loadMethodNames) { + MethodNames = t.GetMethods() + .Where(x => !x.IsSpecialName && x.ReturnType.InheritsFromOrImplements()) + .Select(x => x.Name) + .Where(x => x != "GetInvokingContext") + .Ordered() + .ToList() + .AsReadOnly(); + } + } + + public override string ToString() => FullName ?? Name; + } +} diff --git a/Serialization/Config.cs b/Serialization/Config.cs new file mode 100644 index 0000000..aff8f86 --- /dev/null +++ b/Serialization/Config.cs @@ -0,0 +1,70 @@ +using Periscope.Debuggee; +using System; +using System.Collections.Generic; +using ZSpitz.Util; + +namespace ParseTreeVisualizer.Serialization { + [Serializable] +#if VISUALIZER_DEBUGGEE + public class Config : Periscope.Debuggee.ConfigBase { +#else + public class Config { +#endif + + public string? SelectedParserName { get; set; } + public string? ParseTokensWithRule { get; set; } + public string? SelectedLexerName { get; set; } + public bool ShowTextTokens { get; set; } = true; + public bool ShowWhitespaceTokens { get; set; } = true; + public bool ShowErrorTokens { get; set; } = true; + public HashSet SelectedTokenTypes { get; } = new HashSet(); + public bool ShowTreeTextTokens { get; set; } = true; + public bool ShowTreeWhitespaceTokens { get; set; } = true; + public bool ShowTreeErrorTokens { get; set; } = true; + public bool ShowRuleContextNodes { get; set; } = true; + public HashSet SelectedRuleContexts { get; } = new HashSet(); + public string? RootNodePath { get; set; } + + public bool HasTreeFilter() => !(ShowTreeErrorTokens && ShowTreeWhitespaceTokens && ShowTreeTextTokens && ShowRuleContextNodes && SelectedRuleContexts.None()); + public bool HasTokenListFilter() => !(ShowTextTokens && ShowErrorTokens && ShowWhitespaceTokens && SelectedTokenTypes.None()); + +#if VISUALIZER_DEBUGGEE + // TODO should any parts of the config return ConfigDiffStates.NeedsWrite? + public override ConfigDiffStates Diff(Config baseline) => + ( + baseline.SelectedParserName == SelectedParserName && + baseline.SelectedLexerName == SelectedLexerName && + baseline.ShowTextTokens == ShowTextTokens && + baseline.ShowErrorTokens == ShowErrorTokens && + baseline.ShowWhitespaceTokens == ShowWhitespaceTokens && + baseline.ShowTreeTextTokens == ShowTreeTextTokens && + baseline.ShowTreeErrorTokens == ShowTreeErrorTokens && + baseline.ShowTreeWhitespaceTokens == ShowTreeWhitespaceTokens && + baseline.ShowRuleContextNodes == ShowRuleContextNodes && + baseline.ParseTokensWithRule == ParseTokensWithRule && + baseline.SelectedTokenTypes.SetEquals(SelectedTokenTypes) && + baseline.SelectedRuleContexts.SetEquals(SelectedRuleContexts) + ) ? ConfigDiffStates.NoAction : ConfigDiffStates.NeedsTransfer; + + public override Config Clone() { +#else + public Config Clone() { +#endif + var ret = new Config { + SelectedParserName = SelectedParserName, + SelectedLexerName = SelectedLexerName, + ShowTextTokens = ShowTextTokens, + ShowErrorTokens = ShowErrorTokens, + ShowWhitespaceTokens = ShowWhitespaceTokens, + ShowTreeTextTokens = ShowTreeTextTokens, + ShowTreeErrorTokens = ShowTreeErrorTokens, + ShowTreeWhitespaceTokens = ShowTreeWhitespaceTokens, + ShowRuleContextNodes = ShowRuleContextNodes, + ParseTokensWithRule = ParseTokensWithRule + }; + SelectedTokenTypes.AddRangeTo(ret.SelectedTokenTypes); + SelectedRuleContexts.AddRangeTo(ret.SelectedRuleContexts); + return ret; + } + } +} diff --git a/Serialization/Enums.cs b/Serialization/Enums.cs new file mode 100644 index 0000000..cba05f1 --- /dev/null +++ b/Serialization/Enums.cs @@ -0,0 +1,15 @@ +namespace ParseTreeVisualizer.Serialization { + public enum TreeNodeType { + RuleContext, + Token, + ErrorToken, + WhitespaceToken, + Placeholder + } + + public enum FilterStates { + NotMatched, + Matched, + DescendantMatched + } +} diff --git a/Serialization/Extensions.cs b/Serialization/Extensions.cs new file mode 100644 index 0000000..521767a --- /dev/null +++ b/Serialization/Extensions.cs @@ -0,0 +1,36 @@ +using Antlr4.Runtime.Tree; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ParseTreeVisualizer { + public static class Extensions { + public static IEnumerable Children(this IParseTree tree) { + for (var i = 0; i < tree.ChildCount; i++) { + yield return tree.GetChild(i); + } + } + + public static IEnumerable Descendants(this IParseTree tree) { + for (var i = 0; i < tree.ChildCount; i++) { + var child = tree.GetChild(i); + yield return child; + foreach (var descendant in child.Descendants()) { + yield return descendant; + } + } + } + + public static string GetPositionedText(this IParseTree tree, char filler = ' ') { + var sb = new StringBuilder(); + foreach (var descendant in tree.Descendants().OfType()) { + var fillerCharCount = descendant.Payload.StartIndex - sb.Length; + if (fillerCharCount > 0) { + sb.Append(filler, fillerCharCount); + } + sb.Append(descendant.Payload.Text); + } + return sb.ToString(); + } + } +} diff --git a/Serialization/ParseTreeNode.cs b/Serialization/ParseTreeNode.cs new file mode 100644 index 0000000..0bdbb6a --- /dev/null +++ b/Serialization/ParseTreeNode.cs @@ -0,0 +1,121 @@ +using Antlr4.Runtime; +using Antlr4.Runtime.Tree; +using System; +using System.Collections.Generic; +using System.Linq; +using ZSpitz.Util; + +namespace ParseTreeVisualizer.Serialization { + [Serializable] + public class ParseTreeNode { + public static ParseTreeNode GetPlaceholder(ParseTreeNode? actualRoot) => + actualRoot is null ? + throw new ArgumentNullException(nameof(actualRoot)) : + new("(parent nodes)", TreeNodeType.Placeholder, new List { actualRoot }, actualRoot.CharSpan); + + public string? Caption { get; private set; } + public List? Properties { get; } + public List Children { get; private set; } + public (int startTokenIndex, int endTokenIndex) TokenSpan { get; } + public (int startChar, int endChar) CharSpan { get; private set; } + public TreeNodeType? NodeType { get; private set; } + public FilterStates? FilterState { get; } + public string? Path { get; } + + private ParseTreeNode(string caption, TreeNodeType nodeType, List children, (int startChar, int endChar) charSpan) { + Caption = caption; + NodeType = nodeType; + Children = children; + CharSpan = charSpan; + } + public ParseTreeNode(IParseTree tree, List tokens, string[] ruleNames, Dictionary tokenTypeMapping, Config config, Dictionary ruleMapping, string? path) { + if (tree is null) { throw new ArgumentNullException(nameof(tree)); } + if (ruleMapping is null) { throw new ArgumentNullException(nameof(ruleMapping)); } + if (tokens is null) { throw new ArgumentNullException(nameof(tokens)); } + if (config is null) { throw new ArgumentNullException(nameof(config)); } + + var type = tree.GetType(); + + if (tree is ParserRuleContext ruleContext) { + NodeType = TreeNodeType.RuleContext; + + var caption = type.Name; + if (!ruleMapping.TryGetValue(type, out var x)) { + var ruleIndex = (int)(type.GetProperty("RuleIndex")?.GetValue(tree) ?? -1); + if (ruleNames.TryGetValue(ruleIndex, out caption)) { + ruleMapping[type] = (caption, ruleIndex); + } else { + caption = type.Name; + ruleMapping[type] = (null, null); + } + } else { + caption = x.caption; + } + + Caption = caption; + CharSpan = (ruleContext.Start.StartIndex, ruleContext.Stop?.StopIndex ?? int.MaxValue); + } else if (tree is TerminalNodeImpl terminalNode) { + var token = new Token(terminalNode, tokenTypeMapping); + + if (token.IsError) { + Caption = token.Text; + NodeType = TreeNodeType.ErrorToken; + } else { + Caption = $"\"{token.Text}\""; + NodeType = token.IsWhitespace ? TreeNodeType.WhitespaceToken : TreeNodeType.Token; + } + CharSpan = token.Span; + + if (token.ShowToken(config)) { + tokens.Add(token); + } + } + + Path = path; + var pathDelimiter = path.IsNullOrWhitespace() ? "" : "."; + Properties = type.GetProperties().OrderBy(x => x.Name).Select(prp => new PropertyValue(tree, prp)).ToList(); + Children = tree.Children() + .Select((x, index) => new ParseTreeNode(x, tokens, ruleNames, tokenTypeMapping, config, ruleMapping, $"{path}{pathDelimiter}{index}")) + .Where(x => x.FilterState != FilterStates.NotMatched) // intentionally doesn't exclude null + .ToList(); + TokenSpan = (tree.SourceInterval.a, tree.SourceInterval.b); + + var matched = true; + if (config.HasTreeFilter()) { + matched = NodeType switch { + TreeNodeType.RuleContext => + config.ShowRuleContextNodes && ( + config.SelectedRuleContexts.None() || + type.FullName.In(config.SelectedRuleContexts) + ), + TreeNodeType.ErrorToken => config.ShowErrorTokens, + TreeNodeType.WhitespaceToken => config.ShowTreeWhitespaceTokens, + _ => config.ShowTreeTextTokens + }; + + FilterState = + matched ? + FilterStates.Matched : + (Children.Any(x => x.FilterState.In(FilterStates.Matched, FilterStates.DescendantMatched)) ? + FilterStates.DescendantMatched : + FilterStates.NotMatched); + } + + var toPromote = Children + .Select((child, index) => (grandchild: child.Children.SingleOrDefaultExt(x => x.FilterState.In(FilterStates.Matched, FilterStates.DescendantMatched)), index)) + .WhereT((grandchild, index) => grandchild != null) + .ToList(); + foreach (var (grandchild, index) in toPromote) { + Children[index] = grandchild; + } + } + + public string Stringify(int indentLevel = 0) { + var ret = new string(' ', indentLevel * 4) + Caption; + foreach (var child in Children) { + ret += "\n" + child.Stringify(indentLevel + 1); + } + return ret; + } + } +} diff --git a/Serialization/PropertyValue.cs b/Serialization/PropertyValue.cs new file mode 100644 index 0000000..c67bc6f --- /dev/null +++ b/Serialization/PropertyValue.cs @@ -0,0 +1,32 @@ +using System; +using System.Reflection; +using static ZSpitz.Util.Functions; +//using static ParseTreeVisualizer.Util.Functions; + +namespace ParseTreeVisualizer.Serialization { + [Serializable] + public class PropertyValue { + public bool Custom { get; } + public string Key { get; } + public string? Value { get; } + public PropertyValue(object instance, PropertyInfo prp) { + if (prp is null) { throw new ArgumentNullException(nameof(prp)); } + + Key = prp.Name; + + // null values map to null strings + // exceptions map to <...> delineated strings + // other values map to result of RenderLiteral + + object? value = null; + try { + value = prp.GetValue(instance); + } catch (Exception e) { + Value = $"<{e.GetType()}: {e.Message}>"; + } + if (value is { }) { Value = StringValue(value, "C#"); } + + Custom = !prp.DeclaringType?.Namespace?.StartsWith("Antlr4", StringComparison.Ordinal) ?? false; + } + } +} diff --git a/Serialization/Serialization.projitems b/Serialization/Serialization.projitems new file mode 100644 index 0000000..61a6d70 --- /dev/null +++ b/Serialization/Serialization.projitems @@ -0,0 +1,21 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + c40d4406-b8a2-4ee4-878b-2c89c4360403 + + + ParseTreeVisualizer.Serialization + + + + + + + + + + + + \ No newline at end of file diff --git a/Serialization/Serialization.shproj b/Serialization/Serialization.shproj new file mode 100644 index 0000000..888a935 --- /dev/null +++ b/Serialization/Serialization.shproj @@ -0,0 +1,13 @@ + + + + c40d4406-b8a2-4ee4-878b-2c89c4360403 + 14.0 + + + + + + + + diff --git a/Serialization/Token.cs b/Serialization/Token.cs new file mode 100644 index 0000000..bf12b54 --- /dev/null +++ b/Serialization/Token.cs @@ -0,0 +1,53 @@ +using Antlr4.Runtime; +using Antlr4.Runtime.Tree; +using System; +using System.Collections.Generic; +using ZSpitz.Util; + +namespace ParseTreeVisualizer.Serialization { + [Serializable] + public class Token { + public int Index { get; } + public string TokenType { get; } + public int TokenTypeID { get; } + public int Line { get; } + public int Col { get; } + public string Text { get; } + public bool IsError { get; } + public (int start, int stop) Span { get; } + public bool IsWhitespace { get; } + + public Token(IToken itoken, Dictionary? tokenTypeMapping) { + Index = itoken.TokenIndex; + TokenTypeID = itoken.Type; + Line = itoken.Line; + Col = itoken.Column; + Text = itoken.Text.ToVerbatimString("C#")[1..^1]; + Span = (itoken.StartIndex, itoken.StopIndex); + IsWhitespace = itoken.Text.IsNullOrWhitespace(); + + var tokenType = ""; + TokenType = + tokenTypeMapping?.TryGetValue(TokenTypeID, out tokenType) ?? false ? + tokenType : + $"{TokenTypeID}"; + } + + public Token(TerminalNodeImpl terminalNode, Dictionary tokenTypeMapping) : this(terminalNode.Payload, tokenTypeMapping) { + if (terminalNode is ErrorNodeImpl) { + IsError = true; + } + } + + public bool ShowToken(Config config) { + if (!config.HasTokenListFilter()) { return true; } + var showToken = + IsError ? config.ShowErrorTokens : + IsWhitespace ? config.ShowWhitespaceTokens : + config.ShowTextTokens; + showToken &= config.SelectedTokenTypes.None() || TokenTypeID.In(config.SelectedTokenTypes); + + return showToken; + } + } +} diff --git a/Serialization/VisualizerData.cs b/Serialization/VisualizerData.cs new file mode 100644 index 0000000..f4a25c8 --- /dev/null +++ b/Serialization/VisualizerData.cs @@ -0,0 +1,205 @@ +using Antlr4.Runtime; +using Antlr4.Runtime.Tree; +using System; +using System.Collections.Generic; +using System.Linq; +using static System.Linq.Enumerable; +using ZSpitz.Util; +using static ZSpitz.Util.Functions; + +namespace ParseTreeVisualizer.Serialization { + [Serializable] + public class VisualizerData { + public string Source { get; } + public Config Config { get; } + public ParseTreeNode? Root { get; } + public List? Tokens { get; } + public int SourceOffset { get; } + public List AvailableParsers { get; } = new List(); + public List AvailableLexers { get; } = new List(); + public List AssemblyLoadErrors { get; } = new List(); + public Dictionary? TokenTypeMapping { get; private set; } + public List? UsedRuleContexts { get; } + public bool CanSelectLexer { get; } + public bool CanSelectParser { get; } + + private static readonly string[] ignnoreLoadErrors = new[] { + "Microsoft.Xaml.Behaviors" + }; + + public VisualizerData(object o, Config config) { + if (config is null) { throw new ArgumentNullException(nameof(config)); } + + Config = config; + + Type[] types; + T createInstance(string typename, object[]? args = null) => + types.Single(x => x.FullName == typename).CreateInstance(args); + + { + var baseTypes = new[] { typeof(Parser), typeof(Lexer) }; + types = AppDomain.CurrentDomain.GetAssemblies() + .Where(x => x != GetType().Assembly) + .SelectMany(x => { + var ret = Empty(); + if (!x.FullName.StartsWithAny(ignnoreLoadErrors)) { + try { + ret = x.GetTypes(); + } catch { + AssemblyLoadErrors.Add(x.FullName); + } + } + return ret; + }) + .Where(x => !x.IsAbstract) + .ToArray(); + foreach (var t in types) { + if (t.InheritsFromOrImplements()) { + AvailableParsers.Add(new ClassInfo(t, null, null, true)); + } else if (t.InheritsFromOrImplements()) { + AvailableLexers.Add(new ClassInfo(t)); + } + } + + Config.SelectedLexerName = checkSelection(AvailableLexers, Config.SelectedLexerName); + Config.SelectedParserName = checkSelection(AvailableParsers, Config.SelectedParserName); + if (!Config.SelectedParserName.IsNullOrWhitespace()) { + var parserInfo = AvailableParsers.SingleOrDefaultExt(x => x.FullName == Config.SelectedParserName); + if (parserInfo is null) { + Config.ParseTokensWithRule = null; + } else if (parserInfo.MethodNames is not null) { + if (Config.ParseTokensWithRule.NotIn(parserInfo.MethodNames)) { + Config.ParseTokensWithRule = null; + } + if (Config.ParseTokensWithRule is null) { + Config.ParseTokensWithRule = parserInfo.MethodNames.SingleOrDefaultExt(); + } + } + } + } + + string? source = null; + BufferedTokenStream? tokenStream = null; + IParseTree? tree = null; + IVocabulary? vocabulary = null; + + // these three are mutually exclusive + switch (o) { + case string source1: + source = source1; + CanSelectLexer = true; + CanSelectParser = true; + Source = source; + break; + case BufferedTokenStream tokenStream1: + tokenStream = tokenStream1; + CanSelectParser = true; + Source = tokenStream.TokenSource.InputStream.ToString(); + break; + case IParseTree tree1: + tree = tree1; + Source = tree.GetPositionedText(); + break; + default: + throw new ArgumentException("Unhandled type"); + } + + if (source != null && !Config.SelectedLexerName.IsNullOrWhitespace()) { + var input = new AntlrInputStream(source); + var lexer = createInstance(Config.SelectedLexerName!, new[] { input }); + tokenStream = new CommonTokenStream(lexer); + vocabulary = lexer.Vocabulary; + } + + if ( + tokenStream != null && + !Config.SelectedParserName.IsNullOrWhitespace() && + !Config.ParseTokensWithRule.IsNullOrWhitespace() + ) { + var parser = createInstance(Config.SelectedParserName, new[] { tokenStream }); + tree = (IParseTree)parser.GetType().GetMethod(Config.ParseTokensWithRule)!.Invoke(parser, EmptyArray())!; + vocabulary = parser.Vocabulary; + } + + if (tree is null && tokenStream is null) { + return; + } + + if (tree is null) { + tokenStream!.Fill(); + Tokens = tokenStream.GetTokens() + .Select(token => new Token(token, getTokenTypeMapping())) + .Where(token => token.ShowToken(config)) + .ToList(); + return; + } + + if (!config.RootNodePath.IsNullOrWhitespace()) { + var pathParts = config.RootNodePath.Split('.').Select(x => + int.TryParse(x, out var ret) ? + ret : + -1 + ).ToArray(); + foreach (var pathPart in pathParts) { + var nextTree = tree.GetChild(pathPart); + if (nextTree == null) { + break; + } + tree = tree.GetChild(pathPart); + } + } + + var parserType = tree.GetType().DeclaringType; + Config.SelectedParserName = parserType.FullName; + if (vocabulary is null) { + vocabulary = parserType.GetField("DefaultVocabulary").GetValue(null) as IVocabulary; + } + + var tokenTypeMapping = getTokenTypeMapping()!; + + var ruleNames = (parserType.GetField("ruleNames").GetValue(null) as string[])!; + var rulenameMapping = new Dictionary(); + Tokens = new List(); + var actualRoot = new ParseTreeNode(tree, Tokens, ruleNames, tokenTypeMapping, config, rulenameMapping, Config.RootNodePath); + if (config.RootNodePath.IsNullOrWhitespace()) { + Root = actualRoot; + } else { + Root = ParseTreeNode.GetPlaceholder(actualRoot); + SourceOffset = actualRoot.CharSpan.startChar; + } + + UsedRuleContexts = rulenameMapping.Keys + .Select(x => { + rulenameMapping.TryGetValue(x, out var y); + return new ClassInfo(x, y.name, y.index); + }) + .OrderBy(x => x.Name) + .ToList(); + + Dictionary? getTokenTypeMapping() { + if (vocabulary is null) { return null; } +#if ANTLR_RUNTIME + var maxTokenType = vocabulary.MaxTokenType; +#else + var maxTokenType = (vocabulary as Vocabulary)!.getMaxTokenType(); +#endif + TokenTypeMapping = Range(1, maxTokenType).Select(x => (x, vocabulary.GetSymbolicName(x))).ToDictionary(); + return TokenTypeMapping; + } + + } + + private string? checkSelection(List lst, string? selected) { + if (lst.None(x => x.FullName == selected)) { + selected = null; + } + if (selected.IsNullOrWhitespace()) { + selected = ( + lst.SingleOrDefaultExt(x => x.Antlr != "Runtime") ?? + lst.SingleOrDefaultExt() + )?.FullName; + } + return selected; + } + } +} diff --git a/Shared/Util/Extensions/IEnumerableT.cs b/Shared/Util/Extensions/IEnumerableT.cs index 551bd9e..5a552a4 100644 --- a/Shared/Util/Extensions/IEnumerableT.cs +++ b/Shared/Util/Extensions/IEnumerableT.cs @@ -30,7 +30,7 @@ public static bool None(this IEnumerable src, Func? predicate = n /// (unlike the standard SingleOrDefault, which will throw an exception on multiple elements). /// [return: MaybeNull] - public static T OneOrDefault(this IEnumerable? src, Func? predicate = null) { + public static T OneOrDefault(this IEnumerable? src, Func? predicate = null) { if (src == null) { return default!; } if (predicate != null) { src = src.Where(predicate); } T ret = default!; diff --git a/UI/ParserRuleDisplayNameSelector.cs b/UI/ParserRuleDisplayNameSelector.cs new file mode 100644 index 0000000..073d348 --- /dev/null +++ b/UI/ParserRuleDisplayNameSelector.cs @@ -0,0 +1,14 @@ +using System.Windows; +using System.Windows.Controls; + + +namespace ParseTreeVisualizer { + public class ParserRuleDisplayNameSelector : DataTemplateSelector { + public DataTemplate? RuleNameTemplate { get; set; } + public DataTemplate? TypeNameTemplate { get; set; } + public override DataTemplate? SelectTemplate(object item, DependencyObject container) => + (item as ClassInfo)?.RuleName == null ? + TypeNameTemplate : + RuleNameTemplate; + } +} diff --git a/UI/UI.projitems b/UI/UI.projitems new file mode 100644 index 0000000..1f96507 --- /dev/null +++ b/UI/UI.projitems @@ -0,0 +1,28 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + ae2cd469-cc05-4bac-b8b9-7b07b641c693 + + + ParseTreeVisualizer.UI + + + + + + + + + + VisualizerControl.xaml + + + + + Designer + MSBuild:Compile + + + \ No newline at end of file diff --git a/UI/UI.shproj b/UI/UI.shproj new file mode 100644 index 0000000..b4aa9af --- /dev/null +++ b/UI/UI.shproj @@ -0,0 +1,13 @@ + + + + ae2cd469-cc05-4bac-b8b9-7b07b641c693 + 14.0 + + + + + + + + diff --git a/UI/ViewModels/ConfigViewModel.cs b/UI/ViewModels/ConfigViewModel.cs new file mode 100644 index 0000000..8885cab --- /dev/null +++ b/UI/ViewModels/ConfigViewModel.cs @@ -0,0 +1,86 @@ +using ParseTreeVisualizer.Util; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using static System.IO.Path; + +namespace ParseTreeVisualizer { + public class ConfigViewModel : ViewModelBase { + readonly Config _originalValues; + public ConfigViewModel(Config config, VisualizerData visualizerData) : + this(config, visualizerData.TokenTypeMapping, visualizerData.UsedRuleContexts, visualizerData.AvailableLexers, visualizerData.AvailableParsers, visualizerData.CanSelectLexer, visualizerData.CanSelectParser) { } + + public ConfigViewModel(Config config, Dictionary? tokenTypeMapping, List? ruleContexts, List lexers, List parsers, bool canSelectLexer, bool canSelectParser) : base(config.Clone()) { + TokenTypes = tokenTypeMapping?.SelectKVP((index, text) => { + var ret = new TokenTypeViewModel(index, text) { + IsSelected = index.In(Model.SelectedTokenTypes) + }; + ret.PropertyChanged += (s, e) => { + if (e.PropertyName == "IsSelected") { + Model.SelectedTokenTypes.AddRemove(ret.IsSelected, ret.Index); + } + }; + return ret; + }).OrderBy(x => x.Text).ToList().AsReadOnly(); + + RuleContexts = ruleContexts?.Select(x => { + var ret = new Selectable(x) { + IsSelected = x.FullName.In(Model.SelectedRuleContexts) + }; + ret.PropertyChanged += (s, e) => { + if (e.PropertyName == "IsSelected") { + Model.SelectedRuleContexts.AddRemove(ret.IsSelected, x.FullName); + } + }; + return ret; + }).ToList().AsReadOnly(); + + AvailableLexers = getVMList(lexers); + AvailableParsers = getVMList(parsers); + + CanSelectLexer = canSelectLexer; + CanSelectParser = canSelectParser; + + _originalValues = config; + } + + private ReadOnlyCollection getVMList(List models) { + var lst = models.OrderBy(x => x.Name).ToList(); + lst.Insert(0, ClassInfo.None); + return lst.AsReadOnly(); + } + + public ReadOnlyCollection? TokenTypes { get; } + public ReadOnlyCollection>? RuleContexts { get; } + public ReadOnlyCollection AvailableParsers { get; } + public ReadOnlyCollection AvailableLexers { get; } + + public string Version => GetType().Assembly.GetName().Version.ToString(); + public string Location => GetType().Assembly.Location; + public string Filename => GetFileName(Location); + + public bool IsDirty { + get { + var m = Model; + var o = _originalValues; + return + o.SelectedParserName != m.SelectedParserName || + o.SelectedLexerName != m.SelectedLexerName || + o.ShowTextTokens != m.ShowTextTokens || + o.ShowErrorTokens != m.ShowErrorTokens || + o.ShowWhitespaceTokens != m.ShowWhitespaceTokens || + o.ShowTreeErrorTokens != m.ShowTreeErrorTokens || + o.ShowTreeTextTokens != m.ShowTreeTextTokens || + o.ShowTreeWhitespaceTokens != m.ShowTreeWhitespaceTokens || + o.ShowRuleContextNodes != m.ShowRuleContextNodes || + o.ParseTokensWithRule != m.ParseTokensWithRule || + + !o.SelectedTokenTypes.SetEquals(m.SelectedTokenTypes) || + !o.SelectedRuleContexts.SetEquals(m.SelectedRuleContexts); + } + } + + public bool CanSelectLexer { get; } + public bool CanSelectParser { get; } + } +} diff --git a/UI/ViewModels/ParseTreeNodeViewModel.cs b/UI/ViewModels/ParseTreeNodeViewModel.cs new file mode 100644 index 0000000..db7d9ce --- /dev/null +++ b/UI/ViewModels/ParseTreeNodeViewModel.cs @@ -0,0 +1,34 @@ +using ParseTreeVisualizer.Util; +using System.Collections.ObjectModel; +using System.Linq; + +namespace ParseTreeVisualizer { + public class ParseTreeNodeViewModel : Selectable { + public static ParseTreeNodeViewModel? Create(ParseTreeNode? model) => + model is null ? + null : + new ParseTreeNodeViewModel(model); + + public ParseTreeNodeViewModel(ParseTreeNode model) : base(model) => + Children = (model?.Children.Select(x => new ParseTreeNodeViewModel(x)) ?? Enumerable.Empty()).ToList().AsReadOnly(); + + public ReadOnlyCollection Children { get; } + + public void ClearSelection() { + IsSelected = false; + foreach (var child in Children) { + child.ClearSelection(); + } + } + + private bool isExpanded; + public bool IsExpanded { + get => isExpanded; + set => NotifyChanged(ref isExpanded, value); + } + public void SetSubtreeExpanded(bool expand) { + IsExpanded = expand; + Children.ForEach(x => x.SetSubtreeExpanded(expand)); + } + } +} diff --git a/UI/ViewModels/TokenTypeViewModel.cs b/UI/ViewModels/TokenTypeViewModel.cs new file mode 100644 index 0000000..4bc3522 --- /dev/null +++ b/UI/ViewModels/TokenTypeViewModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using ParseTreeVisualizer.Util; +using static ParseTreeVisualizer.Util.Functions; + +namespace ParseTreeVisualizer { + public class TokenTypeViewModel : Selectable> { + public int Index { get; } + public string Text { get; } + + public TokenTypeViewModel(int index, string text): this(KVP(index,text)) { } + + public TokenTypeViewModel(KeyValuePair tokenType) : base(tokenType) => + (Index, Text) = tokenType; + } +} diff --git a/UI/ViewModels/TokenViewModel.cs b/UI/ViewModels/TokenViewModel.cs new file mode 100644 index 0000000..fe5fe37 --- /dev/null +++ b/UI/ViewModels/TokenViewModel.cs @@ -0,0 +1,15 @@ +using ParseTreeVisualizer.Util; + +namespace ParseTreeVisualizer { + public class TokenViewModel : Selectable { + // we're deliberately not including IsError and TokenTypeID in the ViewModel; we don't want to display it in the autogenerated columns of the DataGrid + public int Index => Model.Index; + public string TokenType => Model.TokenType; + public int Line => Model.Line; + public int Col => Model.Col; + public string Text => Model.Text; + public (int start, int stop) Span => Model.Span; + public bool IsWhitespace => Model.IsWhitespace; + public TokenViewModel(Token model) : base(model) { } + } +} diff --git a/UI/ViewModels/VisualizerDataViewModel.cs b/UI/ViewModels/VisualizerDataViewModel.cs new file mode 100644 index 0000000..afe344c --- /dev/null +++ b/UI/ViewModels/VisualizerDataViewModel.cs @@ -0,0 +1,139 @@ +using ParseTreeVisualizer.Util; +using System; +using System.Collections; +using System.Collections.ObjectModel; +using System.Linq; + +namespace ParseTreeVisualizer { + public class VisualizerDataViewModel : ViewModelBase { + private int sourceSelectionStart; + public int SourceSelectionStart { + get => sourceSelectionStart; + set => NotifyChanged(ref sourceSelectionStart, value); + } + + private int sourceSelectionLength; + public int SourceSelectionLength { + get => sourceSelectionLength; + set => NotifyChanged(ref sourceSelectionLength, value); + } + + private int sourceSelectionEnd => + sourceSelectionLength == 0 ? + sourceSelectionStart : + sourceSelectionStart + sourceSelectionLength - 1; + + private TokenViewModel? firstSelectedToken; + public TokenViewModel? FirstSelectedToken => firstSelectedToken; + + public ParseTreeNodeViewModel? Root { get; } + public ReadOnlyCollection? Tokens { get; } + + public VisualizerDataViewModel(VisualizerData visualizerData) : base(visualizerData) { + Root = ParseTreeNodeViewModel.Create(visualizerData.Root); + Tokens = visualizerData.Tokens?.OrderBy(x => x.Index).Select(x => { + var vm = new TokenViewModel(x); + vm.PropertyChanged += (s, e) => { + if (e.PropertyName != nameof(vm.IsSelected)) { return; } + + if (vm.IsSelected) { + if (firstSelectedToken is null || firstSelectedToken.Model.Index > vm.Model.Index) { + NotifyChanged(ref firstSelectedToken, vm, nameof(FirstSelectedToken)); + } + } else { + if (firstSelectedToken != null && firstSelectedToken.Model.Index == vm.Model.Index) { + var firstSelected = Tokens.Where(x => x.IsSelected).OrderBy(x => x.Model.Index).FirstOrDefault(); + NotifyChanged(ref firstSelectedToken, firstSelected, nameof(FirstSelectedToken)); + } + } + }; + return vm; + }).ToList().AsReadOnly(); + + if (!(Root is null)) { + if (visualizerData.Config.HasTreeFilter()) { + Root.SetSubtreeExpanded(true); + } else { + Root.IsExpanded = true; + } + } + + ChangeSelection = new RelayCommand(sender => updateSelection(sender)); + } + + private bool inUpdateSelection; + private void updateSelection(object parameter) { + if (inUpdateSelection) { return; } + inUpdateSelection = true; + + // sender will be either VisualizerDataViewModel, treenode viewmodel, or token viewmodel + // HACK the type of the sender also tells us which part of the viewmodel has been selected -- source, tree, or tokens + + (int start, int end)? charSpan = null; + string source; + if (parameter == this) { // textbox's data context + charSpan = (sourceSelectionStart, sourceSelectionEnd); + source = "Source"; + } else if (parameter is ParseTreeNodeViewModel selectedNode) { // treeview.SelectedItem + charSpan = selectedNode.Model?.CharSpan; + source = "Root"; + } else if (parameter is IList) { // selected items in datagrid + charSpan = Tokens?.SelectionCharSpan(); + source = "Tokens"; + } else if (parameter is null) { + inUpdateSelection = false; + return; + } else { + throw new Exception("Unknown sender"); + } + + var (start, end) = (charSpan ?? (-1, -1)); + if (source != "Source") { + if (charSpan != null && charSpan != (-1, -1)) { + SourceSelectionStart = start - Model.SourceOffset; + SourceSelectionLength = end - start + 1; + } else { + SourceSelectionLength = 0; + SourceSelectionStart = 0; + } + } + + if (source != "Tokens" && !(Tokens is null)) { + if (charSpan == null) { + Tokens.ForEach(x => x.IsSelected = false); + } else { + Tokens.ForEach(x => + x.IsSelected = x.Model.Span.start <= end && x.Model.Span.stop >= start + ); + } + } + + if (source != "Root" && !(Root?.Model is null)) { + Root.ClearSelection(); + var selectedNode = Root; + + // returns true if the node encompasses the selection + bool matcher(ParseTreeNodeViewModel x) { + var (nodeStart, nodeEnd) = x.Model.CharSpan; + return start >= nodeStart && end <= nodeEnd; + } + + if (matcher(selectedNode)) { + while (true) { + var nextChild = selectedNode.Children.OneOrDefault(matcher); + if (nextChild is null) { break; } + selectedNode = nextChild; + selectedNode.IsExpanded = true; + } + selectedNode.IsSelected = true; + } + } + + inUpdateSelection = false; + } + + // TOOD move filtering to here + + public RelayCommand ChangeSelection { get; } + } +} diff --git a/UI/VisualizerControl.xaml b/UI/VisualizerControl.xaml new file mode 100644 index 0000000..63cc592 --- /dev/null +++ b/UI/VisualizerControl.xaml @@ -0,0 +1,268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/UI/VisualizerControl.xaml.cs b/UI/VisualizerControl.xaml.cs new file mode 100644 index 0000000..8f7cea6 --- /dev/null +++ b/UI/VisualizerControl.xaml.cs @@ -0,0 +1,144 @@ +using System; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using Microsoft.VisualStudio.DebuggerVisualizers; +using ParseTreeVisualizer.Util; + +namespace ParseTreeVisualizer { + public partial class VisualizerControl { + public VisualizerControl() { + InitializeComponent(); + + tokens.AutoGeneratingColumn += (s, e) => { + if (e.PropertyName.In( + nameof(ViewModelBase.Model), + nameof(Selectable.IsSelected) + )) { + e.Cancel = true; + } + + if (e.PropertyName == nameof(TokenViewModel.Text)) { + e.Column.Width = 150; + (e.Column as DataGridTextColumn)!.ElementStyle = FindResource("TextTrimmedTextbox") as Style; + } + }; + + // scrolls the tree view item into view when selected + AddHandler(TreeViewItem.SelectedEvent, (RoutedEventHandler)((s, e) => ((TreeViewItem)e.OriginalSource).BringIntoView())); + + Loaded += (s, e) => { + // https://stackoverflow.com/a/21436273/111794 + configPopup.CustomPopupPlacementCallback += (popupSize, targetSize, offset) => + new[] { + new CustomPopupPlacement() { + Point = new Point(targetSize.Width - popupSize.Width, targetSize.Height) + } + }; + configButton.Click += (s1, e1) => configPopup.IsOpen = true; + + configPopup.Opened += (s1, e1) => + configPopup.DataContext = new ConfigViewModel(data.Model.Config, data.Model); + + configPopup.Closed += (s1, e1) => { + var popupConfig = configPopup.DataContext(); + if (popupConfig.IsDirty) { + Config = popupConfig.Model; + } + }; + + source.LostFocus += (s1, e1) => e1.Handled = true; + source.Focus(); + source.SelectAll(); + }; + + Unloaded += (s, e) => Config?.Write(); + } + + private VisualizerDataViewModel data => (VisualizerDataViewModel)DataContext; + + private void LoadDataContext() { + if (_objectProvider is null || config is null) { return; } + DataContext = null; + if (!(_objectProvider.TransferObject(config) is VisualizerData response)) { + throw new InvalidOperationException("Unspecified error while serializing/deserializing"); + } + + var vm = new VisualizerDataViewModel(response); + vm.PropertyChanged += (s, e) => { + if (e.PropertyName != nameof(VisualizerDataViewModel.FirstSelectedToken)) { return; } + if (vm.FirstSelectedToken is null) { return; } + tokens.ScrollIntoView(vm.FirstSelectedToken); + }; + + DataContext = vm; + config = data.Model.Config; + Config?.Write(); + + var assemblyLoadErrors = data.Model.AssemblyLoadErrors; + if (assemblyLoadErrors.Any()) { + MessageBox.Show($"Error loading the following assemblies:\n\n{assemblyLoadErrors.Joined("\n")}"); + } + } + + private IVisualizerObjectProvider? _objectProvider; + public IVisualizerObjectProvider? objectProvider { + get => _objectProvider; + set { + if (value == _objectProvider) { return; } + _objectProvider = value; + LoadDataContext(); + } + } + + private Config? config; + public Config? Config { + get => config; + set { + if (value == config) { return; } + config = value; + LoadDataContext(); + } + } + + private void ExpandAll(object sender, RoutedEventArgs e) => + ((MenuItem)sender).DataContext()?.SetSubtreeExpanded(true); + private void CollapseAll(object sender, RoutedEventArgs e) => + ((MenuItem)sender).DataContext()?.SetSubtreeExpanded(false); + private void SetRootNode(object sender, RoutedEventArgs e) { + if (Config is null) { return; } + Config.RootNodePath = ((MenuItem)sender).DataContext()?.Model.Path; + LoadDataContext(); + } + private void OpenRootNewWindow(object sender, RoutedEventArgs e) { + if (Config is null) { return; } + var newWindow = new VisualizerWindow(); + var content = (newWindow.Content as VisualizerControl)!; + content.Config = Config.Clone(); + content.Config.RootNodePath = ((MenuItem)sender).DataContext()?.Model.Path; + content.objectProvider = objectProvider; + newWindow.ShowDialog(); + } + + private void CopyWatchExpression(object sender, RoutedEventArgs e) { + if (Config is null) { return; } + + var node = ((MenuItem)sender).DataContext(); + if (node == null) { return; } + var model = node.Model; + + if (data.Model.Config.WatchBaseExpression.IsNullOrWhitespace()) { + var dlg = new WatchExpressionPrompt(); + dlg.ShowDialog(); + data.Model.Config.WatchBaseExpression = dlg.Expression; + } + + var watchExpression = Config.WatchBaseExpression; + if (!model.Path.IsNullOrWhitespace()) { + watchExpression += model.Path.Split('.').Joined("", x => $".GetChild({x})"); + } + Clipboard.SetText(watchExpression); + } + } +}