From 6a4467b60cc3d7c67b753e68284363493d027b1d Mon Sep 17 00:00:00 2001 From: Andrey Shchekin Date: Tue, 20 Jun 2017 21:59:21 +1200 Subject: [PATCH] [gh-125] Implemented AST-hover for Roslyn. --- .../Decompilation/AstOnly/RoslynAstTarget.cs | 20 +- source/Server/Server.csproj | 2 +- source/Tests/TestCode/Ast/EmptyClass.cs2ast | 36 +++- .../Tests/TestCode/Ast/LiteralTokens.cs2ast | 189 ++++++++++++++++-- .../TestCode/Ast/StructuredTrivia.cs2ast | 33 ++- source/WebApp/index.html | 33 ++- source/WebApp/js/app.js | 16 +- .../WebApp/js/ui/components/app-ast-view.js | 77 ++++--- .../js/ui/components/app-mirrorsharp.js | 22 +- .../components/internal/app-ast-view-item.js | 28 +++ source/WebApp/less/imports/ast.less | 15 +- source/WebApp/less/imports/codemirror.less | 4 + source/WebApp/less/imports/common.less | 1 + 13 files changed, 398 insertions(+), 78 deletions(-) create mode 100644 source/WebApp/js/ui/components/internal/app-ast-view-item.js diff --git a/source/Server/Decompilation/AstOnly/RoslynAstTarget.cs b/source/Server/Decompilation/AstOnly/RoslynAstTarget.cs index 20f9490..f4c3873 100644 --- a/source/Server/Decompilation/AstOnly/RoslynAstTarget.cs +++ b/source/Server/Decompilation/AstOnly/RoslynAstTarget.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; using MirrorSharp.Advanced; using SharpLab.Server.Decompilation.Internal; @@ -26,6 +27,7 @@ namespace SharpLab.Server.Decompilation.AstOnly { var parentPropertyName = specialParentPropertyName ?? RoslynSyntaxHelper.GetParentPropertyName(node); if (parentPropertyName != null) writer.WriteProperty("property", parentPropertyName); + SerializeSpanProperty(node.FullSpan, writer); writer.WritePropertyStartArray("children"); foreach (var child in node.ChildNodesAndTokens()) { if (child.IsNode) { @@ -47,12 +49,18 @@ namespace SharpLab.Server.Decompilation.AstOnly { if (parentPropertyName != null) writer.WriteProperty("property", parentPropertyName); + SerializeSpanProperty(token.FullSpan, writer); + if (token.HasLeadingTrivia || token.HasTrailingTrivia) { writer.WritePropertyStartArray("children"); foreach (var trivia in token.LeadingTrivia) { SerializeTrivia(trivia, writer); } - writer.WriteValue(token.ToString()); + writer.WriteStartObject(); + writer.WriteProperty("type", "value"); + writer.WriteProperty("value", token.ValueText); + SerializeSpanProperty(token.Span, writer); + writer.WriteEndObject(); foreach (var trivia in token.TrailingTrivia) { SerializeTrivia(trivia, writer); } @@ -68,6 +76,7 @@ namespace SharpLab.Server.Decompilation.AstOnly { writer.WriteStartObject(); writer.WriteProperty("type", "trivia"); writer.WriteProperty("kind", RoslynSyntaxHelper.GetKindName(trivia.RawKind)); + SerializeSpanProperty(trivia.FullSpan, writer); if (trivia.HasStructure) { writer.WritePropertyStartArray("children"); SerializeNode(trivia.GetStructure(), writer, "Structure"); @@ -79,6 +88,15 @@ namespace SharpLab.Server.Decompilation.AstOnly { writer.WriteEndObject(); } + private void SerializeSpanProperty(TextSpan span, IFastJsonWriter writer) { + writer.WritePropertyName("range"); + using (var stringWriter = writer.OpenString()) { + stringWriter.Write(span.Start); + stringWriter.Write(":"); + stringWriter.Write(span.End); + } + } + public IReadOnlyCollection SupportedLanguageNames { get; } = new[] { LanguageNames.CSharp, LanguageNames.VisualBasic diff --git a/source/Server/Server.csproj b/source/Server/Server.csproj index 9569d8c..1f1fea2 100644 --- a/source/Server/Server.csproj +++ b/source/Server/Server.csproj @@ -20,7 +20,7 @@ - + diff --git a/source/Tests/TestCode/Ast/EmptyClass.cs2ast b/source/Tests/TestCode/Ast/EmptyClass.cs2ast index 852915d..2ca398b 100644 --- a/source/Tests/TestCode/Ast/EmptyClass.cs2ast +++ b/source/Tests/TestCode/Ast/EmptyClass.cs2ast @@ -7,19 +7,27 @@ { "type": "node", "kind": "CompilationUnit", + "range": "0:19", "children": [ { "type": "node", "kind": "ClassDeclaration", + "range": "0:19", "children": [ { "type": "token", "kind": "PublicKeyword", + "range": "0:7", "children": [ - "public", + { + "type": "value", + "value": "public", + "range": "0:6" + }, { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "6:7", "value": " " } ] @@ -28,11 +36,17 @@ "type": "token", "kind": "ClassKeyword", "property": "Keyword", + "range": "7:13", "children": [ - "class", + { + "type": "value", + "value": "class", + "range": "7:12" + }, { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "12:13", "value": " " } ] @@ -41,11 +55,17 @@ "type": "token", "kind": "IdentifierToken", "property": "Identifier", + "range": "13:15", "children": [ - "C", + { + "type": "value", + "value": "C", + "range": "13:14" + }, { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "14:15", "value": " " } ] @@ -54,11 +74,17 @@ "type": "token", "kind": "OpenBraceToken", "property": "OpenBraceToken", + "range": "15:18", "children": [ - "{", + { + "type": "value", + "value": "{", + "range": "15:16" + }, { "type": "trivia", "kind": "EndOfLineTrivia", + "range": "16:18", "value": "\r\n" } ] @@ -67,6 +93,7 @@ "type": "token", "kind": "CloseBraceToken", "property": "CloseBraceToken", + "range": "18:19", "value": "}" } ] @@ -75,6 +102,7 @@ "type": "token", "kind": "EndOfFileToken", "property": "EndOfFileToken", + "range": "19:19", "value": "" } ] diff --git a/source/Tests/TestCode/Ast/LiteralTokens.cs2ast b/source/Tests/TestCode/Ast/LiteralTokens.cs2ast index 8394959..192446c 100644 --- a/source/Tests/TestCode/Ast/LiteralTokens.cs2ast +++ b/source/Tests/TestCode/Ast/LiteralTokens.cs2ast @@ -13,20 +13,28 @@ b"; { "type": "node", "kind": "CompilationUnit", + "range": "0:102", "children": [ { "type": "node", "kind": "ClassDeclaration", + "range": "0:102", "children": [ { "type": "token", "kind": "ClassKeyword", "property": "Keyword", + "range": "0:6", "children": [ - "class", + { + "type": "value", + "value": "class", + "range": "0:5" + }, { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "5:6", "value": " " } ] @@ -35,11 +43,17 @@ b"; "type": "token", "kind": "IdentifierToken", "property": "Identifier", + "range": "6:8", "children": [ - "C", + { + "type": "value", + "value": "C", + "range": "6:7" + }, { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "7:8", "value": " " } ] @@ -48,11 +62,17 @@ b"; "type": "token", "kind": "OpenBraceToken", "property": "OpenBraceToken", + "range": "8:11", "children": [ - "{", + { + "type": "value", + "value": "{", + "range": "8:9" + }, { "type": "trivia", "kind": "EndOfLineTrivia", + "range": "9:11", "value": "\r\n" } ] @@ -60,31 +80,41 @@ b"; { "type": "node", "kind": "FieldDeclaration", + "range": "11:27", "children": [ { "type": "node", "kind": "VariableDeclaration", "property": "Declaration", + "range": "11:24", "children": [ { "type": "node", "kind": "PredefinedType", "property": "Type", + "range": "11:19", "children": [ { "type": "token", "kind": "IntKeyword", "property": "Keyword", + "range": "11:19", "children": [ { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "11:15", "value": " " }, - "int", + { + "type": "value", + "value": "int", + "range": "15:18" + }, { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "18:19", "value": " " } ] @@ -94,16 +124,23 @@ b"; { "type": "node", "kind": "VariableDeclarator", + "range": "19:24", "children": [ { "type": "token", "kind": "IdentifierToken", "property": "Identifier", + "range": "19:21", "children": [ - "i", + { + "type": "value", + "value": "i", + "range": "19:20" + }, { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "20:21", "value": " " } ] @@ -112,16 +149,23 @@ b"; "type": "node", "kind": "EqualsValueClause", "property": "Initializer", + "range": "21:24", "children": [ { "type": "token", "kind": "EqualsToken", "property": "EqualsToken", + "range": "21:23", "children": [ - "=", + { + "type": "value", + "value": "=", + "range": "21:22" + }, { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "22:23", "value": " " } ] @@ -130,11 +174,13 @@ b"; "type": "node", "kind": "NumericLiteralExpression", "property": "Value", + "range": "23:24", "children": [ { "type": "token", "kind": "NumericLiteralToken", "property": "Token", + "range": "23:24", "value": "1" } ] @@ -149,11 +195,17 @@ b"; "type": "token", "kind": "SemicolonToken", "property": "SemicolonToken", + "range": "24:27", "children": [ - ";", + { + "type": "value", + "value": ";", + "range": "24:25" + }, { "type": "trivia", "kind": "EndOfLineTrivia", + "range": "25:27", "value": "\r\n" } ] @@ -163,31 +215,41 @@ b"; { "type": "node", "kind": "FieldDeclaration", + "range": "27:46", "children": [ { "type": "node", "kind": "VariableDeclaration", "property": "Declaration", + "range": "27:43", "children": [ { "type": "node", "kind": "PredefinedType", "property": "Type", + "range": "27:36", "children": [ { "type": "token", "kind": "CharKeyword", "property": "Keyword", + "range": "27:36", "children": [ { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "27:31", "value": " " }, - "char", + { + "type": "value", + "value": "char", + "range": "31:35" + }, { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "35:36", "value": " " } ] @@ -197,16 +259,23 @@ b"; { "type": "node", "kind": "VariableDeclarator", + "range": "36:43", "children": [ { "type": "token", "kind": "IdentifierToken", "property": "Identifier", + "range": "36:38", "children": [ - "c", + { + "type": "value", + "value": "c", + "range": "36:37" + }, { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "37:38", "value": " " } ] @@ -215,16 +284,23 @@ b"; "type": "node", "kind": "EqualsValueClause", "property": "Initializer", + "range": "38:43", "children": [ { "type": "token", "kind": "EqualsToken", "property": "EqualsToken", + "range": "38:40", "children": [ - "=", + { + "type": "value", + "value": "=", + "range": "38:39" + }, { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "39:40", "value": " " } ] @@ -233,11 +309,13 @@ b"; "type": "node", "kind": "CharacterLiteralExpression", "property": "Value", + "range": "40:43", "children": [ { "type": "token", "kind": "CharacterLiteralToken", "property": "Token", + "range": "40:43", "value": "'c'" } ] @@ -252,11 +330,17 @@ b"; "type": "token", "kind": "SemicolonToken", "property": "SemicolonToken", + "range": "43:46", "children": [ - ";", + { + "type": "value", + "value": ";", + "range": "43:44" + }, { "type": "trivia", "kind": "EndOfLineTrivia", + "range": "44:46", "value": "\r\n" } ] @@ -266,36 +350,47 @@ b"; { "type": "node", "kind": "FieldDeclaration", + "range": "46:75", "children": [ { "type": "node", "kind": "VariableDeclaration", "property": "Declaration", + "range": "46:72", "children": [ { "type": "node", "kind": "PredefinedType", "property": "Type", + "range": "46:59", "children": [ { "type": "token", "kind": "StringKeyword", "property": "Keyword", + "range": "46:59", "children": [ { "type": "trivia", "kind": "EndOfLineTrivia", + "range": "46:48", "value": "\r\n" }, { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "48:52", "value": " " }, - "string", + { + "type": "value", + "value": "string", + "range": "52:58" + }, { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "58:59", "value": " " } ] @@ -305,16 +400,23 @@ b"; { "type": "node", "kind": "VariableDeclarator", + "range": "59:72", "children": [ { "type": "token", "kind": "IdentifierToken", "property": "Identifier", + "range": "59:62", "children": [ - "s1", + { + "type": "value", + "value": "s1", + "range": "59:61" + }, { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "61:62", "value": " " } ] @@ -323,16 +425,23 @@ b"; "type": "node", "kind": "EqualsValueClause", "property": "Initializer", + "range": "62:72", "children": [ { "type": "token", "kind": "EqualsToken", "property": "EqualsToken", + "range": "62:64", "children": [ - "=", + { + "type": "value", + "value": "=", + "range": "62:63" + }, { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "63:64", "value": " " } ] @@ -341,11 +450,13 @@ b"; "type": "node", "kind": "StringLiteralExpression", "property": "Value", + "range": "64:72", "children": [ { "type": "token", "kind": "StringLiteralToken", "property": "Token", + "range": "64:72", "value": "\"a\\r\\nb\"" } ] @@ -360,11 +471,17 @@ b"; "type": "token", "kind": "SemicolonToken", "property": "SemicolonToken", + "range": "72:75", "children": [ - ";", + { + "type": "value", + "value": ";", + "range": "72:73" + }, { "type": "trivia", "kind": "EndOfLineTrivia", + "range": "73:75", "value": "\r\n" } ] @@ -374,31 +491,41 @@ b"; { "type": "node", "kind": "FieldDeclaration", + "range": "75:101", "children": [ { "type": "node", "kind": "VariableDeclaration", "property": "Declaration", + "range": "75:98", "children": [ { "type": "node", "kind": "PredefinedType", "property": "Type", + "range": "75:86", "children": [ { "type": "token", "kind": "StringKeyword", "property": "Keyword", + "range": "75:86", "children": [ { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "75:79", "value": " " }, - "string", + { + "type": "value", + "value": "string", + "range": "79:85" + }, { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "85:86", "value": " " } ] @@ -408,16 +535,23 @@ b"; { "type": "node", "kind": "VariableDeclarator", + "range": "86:98", "children": [ { "type": "token", "kind": "IdentifierToken", "property": "Identifier", + "range": "86:89", "children": [ - "s2", + { + "type": "value", + "value": "s2", + "range": "86:88" + }, { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "88:89", "value": " " } ] @@ -426,16 +560,23 @@ b"; "type": "node", "kind": "EqualsValueClause", "property": "Initializer", + "range": "89:98", "children": [ { "type": "token", "kind": "EqualsToken", "property": "EqualsToken", + "range": "89:91", "children": [ - "=", + { + "type": "value", + "value": "=", + "range": "89:90" + }, { "type": "trivia", "kind": "WhitespaceTrivia", + "range": "90:91", "value": " " } ] @@ -444,11 +585,13 @@ b"; "type": "node", "kind": "StringLiteralExpression", "property": "Value", + "range": "91:98", "children": [ { "type": "token", "kind": "StringLiteralToken", "property": "Token", + "range": "91:98", "value": "@\"a\r\nb\"" } ] @@ -463,11 +606,17 @@ b"; "type": "token", "kind": "SemicolonToken", "property": "SemicolonToken", + "range": "98:101", "children": [ - ";", + { + "type": "value", + "value": ";", + "range": "98:99" + }, { "type": "trivia", "kind": "EndOfLineTrivia", + "range": "99:101", "value": "\r\n" } ] @@ -478,6 +627,7 @@ b"; "type": "token", "kind": "CloseBraceToken", "property": "CloseBraceToken", + "range": "101:102", "value": "}" } ] @@ -486,6 +636,7 @@ b"; "type": "token", "kind": "EndOfFileToken", "property": "EndOfFileToken", + "range": "102:102", "value": "" } ] diff --git a/source/Tests/TestCode/Ast/StructuredTrivia.cs2ast b/source/Tests/TestCode/Ast/StructuredTrivia.cs2ast index 3e8dca8..4c00f08 100644 --- a/source/Tests/TestCode/Ast/StructuredTrivia.cs2ast +++ b/source/Tests/TestCode/Ast/StructuredTrivia.cs2ast @@ -6,35 +6,46 @@ { "type": "node", "kind": "CompilationUnit", + "range": "0:27", "children": [ { "type": "token", "kind": "EndOfFileToken", "property": "EndOfFileToken", + "range": "0:27", "children": [ { "type": "trivia", "kind": "SingleLineDocumentationCommentTrivia", + "range": "0:27", "children": [ { "type": "node", "kind": "SingleLineDocumentationCommentTrivia", "property": "Structure", + "range": "0:27", "children": [ { "type": "node", "kind": "XmlText", + "range": "0:4", "children": [ { "type": "token", "kind": "XmlTextLiteralToken", + "range": "0:4", "children": [ { "type": "trivia", "kind": "DocumentationCommentExteriorTrivia", + "range": "0:3", "value": "///" }, - " " + { + "type": "value", + "value": " ", + "range": "3:4" + } ] } ] @@ -42,27 +53,32 @@ { "type": "node", "kind": "XmlElement", + "range": "4:27", "children": [ { "type": "node", "kind": "XmlElementStartTag", "property": "StartTag", + "range": "4:13", "children": [ { "type": "token", "kind": "LessThanToken", "property": "LessThanToken", + "range": "4:5", "value": "<" }, { "type": "node", "kind": "XmlName", "property": "Name", + "range": "5:12", "children": [ { "type": "token", "kind": "IdentifierToken", "property": "LocalName", + "range": "5:12", "value": "summary" } ] @@ -71,6 +87,7 @@ "type": "token", "kind": "GreaterThanToken", "property": "GreaterThanToken", + "range": "12:13", "value": ">" } ] @@ -78,10 +95,12 @@ { "type": "node", "kind": "XmlText", + "range": "13:17", "children": [ { "type": "token", "kind": "XmlTextLiteralToken", + "range": "13:17", "value": "Test" } ] @@ -90,22 +109,26 @@ "type": "node", "kind": "XmlElementEndTag", "property": "EndTag", + "range": "17:27", "children": [ { "type": "token", "kind": "LessThanSlashToken", "property": "LessThanSlashToken", + "range": "17:19", "value": "" } ] @@ -124,13 +148,18 @@ "type": "token", "kind": "EndOfDocumentationCommentToken", "property": "EndOfComment", + "range": "27:27", "value": "" } ] } ] }, - "" + { + "type": "value", + "value": "", + "range": "27:27" + } ] } ] diff --git a/source/WebApp/index.html b/source/WebApp/index.html index 0664c62..4a850ee 100644 --- a/source/WebApp/index.html +++ b/source/WebApp/index.html @@ -51,6 +51,7 @@ + v-bind:roots="lastResultOfType.ast.value" + v-on:item-hover="applyAstHover"> @@ -144,23 +146,18 @@ Built by Andrey Shchekin (@ashmind). See SharpLab on GitHub. - diff --git a/source/WebApp/js/app.js b/source/WebApp/js/app.js index 2ee6644..e0ce653 100644 --- a/source/WebApp/js/app.js +++ b/source/WebApp/js/app.js @@ -52,6 +52,16 @@ function getServiceUrl(branch) { return `${httpRoot.replace(/^http/, 'ws')}/mirrorsharp`; } +function applyAstHover(item) { + if (!item || !item.range) { + this.highlightedCodeRange = null; + return; + } + + const [start, end] = item.range.split(':'); + this.highlightedCodeRange = { start, end }; +} + async function createAppAsync() { const data = Object.assign({ languages, @@ -70,7 +80,9 @@ async function createAppAsync() { errors: [], warnings: [] }, - lastResultOfType: { code: null, ast: null } + lastResultOfType: { code: null, ast: null }, + + highlightedCodeRange: null }); await state.loadAsync(data); data.lastLoadedCode = data.code; @@ -114,7 +126,7 @@ async function createAppAsync() { return { name: 'default', color: '#4684ee' }; } }, - methods: { applyUpdateResult, applyServerError, applyConnectionChange } + methods: { applyUpdateResult, applyServerError, applyConnectionChange, applyAstHover } }; } diff --git a/source/WebApp/js/ui/components/app-ast-view.js b/source/WebApp/js/ui/components/app-ast-view.js index 3941f28..ec96974 100644 --- a/source/WebApp/js/ui/components/app-ast-view.js +++ b/source/WebApp/js/ui/components/app-ast-view.js @@ -1,44 +1,71 @@ import Vue from 'vue'; +import AstViewItem from './internal/app-ast-view-item.js'; Vue.component('app-ast-view', { props: { roots: Array }, - methods: { - renderValue: function(value, type) { - if (type === 'trivia') - return escapeTrivia(value); - - return escapeCommon(value); - } - }, - data: () => ({ - expanded: [] - }), mounted: function() { + const getItem = li => this.allById[li.getAttribute('data-id')]; + + let hoverSupported = false; + let lastHoverByClick = null; Vue.nextTick(() => { - this.$el.addEventListener('click', e => { - const li = findLI(e); - if (!li) - return; + handleOnLI(this.$el, 'click', li => { li.classList.toggle('collapsed'); - e.stopPropagation(); + if (hoverSupported) + return; + + if (lastHoverByClick) + lastHoverByClick.classList.remove('hover'); + this.$emit('item-hover', getItem(li)); + li.classList.add('hover'); + lastHoverByClick = li; + }); + handleOnLI(this.$el, 'mouseover', li => { + hoverSupported = true; + this.$emit('item-hover', getItem(li)); + li.classList.add('hover'); + }); + handleOnLI(this.$el, 'mouseout', li => { + this.$emit('item-hover', null); + li.classList.remove('hover'); }); }); }, - template: '#app-ast-view' + render: function(h) { + this.allById = {}; + return h('div', [renderTree(h, this.roots, this.allById)]); + } }); -function escapeCommon(value) { - return value - .replace('\r', '\\r') - .replace('\n', '\\n') - .replace('\t', '\\t'); +function renderTree(h, items, allById, parentId) { + return h('ol', + items.map((item, index) => renderLI(h, item, allById, (parentId != null) ? parentId + '.' + index : index)) + ); } -function escapeTrivia(value) { - return escapeCommon(value) - .replace(/(^ +| +$)/g, (_,$1) => $1.length > 1 ? `` : ''); +function renderLI(h, item, allById, id) { + allById[id] = item; + return h('li', + { + class: { collapsed: true, leaf: !item.children }, + attrs: { 'data-id': id } + }, + [ + h(AstViewItem, { props: { item } }), + item.children ? renderTree(h, item.children, allById, id) : null + ] + ); +} + +function handleOnLI(root, event, action) { + root.addEventListener(event, e => { + const li = findLI(e); + if (!li) + return; + action(li); + }); } function findLI(e) { diff --git a/source/WebApp/js/ui/components/app-mirrorsharp.js b/source/WebApp/js/ui/components/app-mirrorsharp.js index 8635024..26ed7eb 100644 --- a/source/WebApp/js/ui/components/app-mirrorsharp.js +++ b/source/WebApp/js/ui/components/app-mirrorsharp.js @@ -4,9 +4,10 @@ import 'codemirror/mode/mllike/mllike'; Vue.component('app-mirrorsharp', { props: { - initialText: String, - serverOptions: Object, - serviceUrl: String + initialText: String, + serverOptions: Object, + serviceUrl: String, + highlightedRange: Object }, mounted: function() { Vue.nextTick(() => { @@ -41,6 +42,21 @@ Vue.component('app-mirrorsharp', { if (this.serverOptions) instance.sendServerOptions(this.serverOptions); }); + + let currentMarker = null; + this.$watch('highlightedRange', range => { + const cm = instance.getCodeMirror(); + if (currentMarker) { + currentMarker.clear(); + currentMarker = null; + } + if (!range) + return; + + const from = cm.posFromIndex(range.start); + const to = cm.posFromIndex(range.end); + currentMarker = cm.markText(from, to, { className: 'highlighted' }); + }); }); }, template: '' diff --git a/source/WebApp/js/ui/components/internal/app-ast-view-item.js b/source/WebApp/js/ui/components/internal/app-ast-view-item.js new file mode 100644 index 0000000..72168bb --- /dev/null +++ b/source/WebApp/js/ui/components/internal/app-ast-view-item.js @@ -0,0 +1,28 @@ +export default { + props: { + item: {} + }, + methods: { + renderValue + }, + template: '#app-ast-view-item' +}; + +function renderValue(value, type) { + if (type === 'trivia') + return escapeTrivia(value); + + return escapeCommon(value); +} + +function escapeCommon(value) { + return value + .replace('\r', '\\r') + .replace('\n', '\\n') + .replace('\t', '\\t'); +} + +function escapeTrivia(value) { + return escapeCommon(value) + .replace(/(^ +| +$)/g, (_,$1) => $1.length > 1 ? `` : ''); +} \ No newline at end of file diff --git a/source/WebApp/less/imports/ast.less b/source/WebApp/less/imports/ast.less index 0d8729b..ed05d0f 100644 --- a/source/WebApp/less/imports/ast.less +++ b/source/WebApp/less/imports/ast.less @@ -7,9 +7,8 @@ bottom: 0; right: 0; overflow: auto; + display: flex; - padding: 0; - margin: 0; margin-left: 4px; margin-top: 2px; @@ -20,13 +19,23 @@ padding-left: 20px; } + > ol { + padding: 0; + } + li { cursor: pointer; - padding-top: 4px; + padding-top: 2px; + padding-bottom: 2px; + padding-right: 6px; &:first-child { padding-top: 2px; } + + &.hover { + background-color: @highlight-color; + } } button { diff --git a/source/WebApp/less/imports/codemirror.less b/source/WebApp/less/imports/codemirror.less index f67c4e4..a097880 100644 --- a/source/WebApp/less/imports/codemirror.less +++ b/source/WebApp/less/imports/codemirror.less @@ -34,4 +34,8 @@ textarea, .CodeMirror { .cm-tag { color: #bbb; } +} + +.highlighted { + background-color: @highlight-color; } \ No newline at end of file diff --git a/source/WebApp/less/imports/common.less b/source/WebApp/less/imports/common.less index 58ca5fc..d2e0358 100644 --- a/source/WebApp/less/imports/common.less +++ b/source/WebApp/less/imports/common.less @@ -2,6 +2,7 @@ @error-color: #dc3912; @warning-color: #ff9900; @offline-color: #aaa; +@highlight-color: #efefef; .code-text() { font-family: Consolas, Menlo, Monaco, monospace;