Commit 0f6afbe1 authored by Zeynep Cankara's avatar Zeynep Cankara Committed by Commit Bot

[tools][system-analyzer] Add Source Code Panel

This CL adds a source code panel to display source code positions of
Map/IC log events.

* Clicking file positions on the Ic Panel emits FocusEvent with
SourcePositionLogEvent as entry to highlight code related with the
selected icLogEvent.

* Clicking map details on the Map Panel emits FocusEvent with
SourcePositionLogEvent as entry to highlight code related with the
selected mapLogEvent.

Bug: v8:10644
Change-Id: Icaf3e9e3f7fae485c50ad685f9ec5dc8ac28b3dc
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2358734
Commit-Queue: Zeynep Cankara <zcankara@google.com>
Reviewed-by: 's avatarSathya Gunasekaran  <gsathya@chromium.org>
Cr-Commit-Position: refs/heads/master@{#69610}
parent 1512f893
...@@ -38,6 +38,8 @@ function Profile() { ...@@ -38,6 +38,8 @@ function Profile() {
this.bottomUpTree_ = new CallTree(); this.bottomUpTree_ = new CallTree();
this.c_entries_ = {}; this.c_entries_ = {};
this.ticks_ = []; this.ticks_ = [];
this.scripts_ = [];
this.urlToScript_ = new Map();
}; };
...@@ -226,8 +228,21 @@ Profile.prototype.addSourcePositions = function ( ...@@ -226,8 +228,21 @@ Profile.prototype.addSourcePositions = function (
/** /**
* Adds script source code. * Adds script source code.
*/ */
Profile.prototype.addScriptSource = function (script, source) { Profile.prototype.addScriptSource = function (scriptId, url, source) {
// CLI does not need source code => ignore. this.scripts_[scriptId] = {
scriptId: scriptId,
name: url,
source: source
};
this.urlToScript_.set(url, source);
};
/**
* Adds script source code.
*/
Profile.prototype.getScript = function (url) {
return this.urlToScript_.get(url);
}; };
/** /**
...@@ -1018,8 +1033,9 @@ JsonProfile.prototype.addSourcePositions = function ( ...@@ -1018,8 +1033,9 @@ JsonProfile.prototype.addSourcePositions = function (
}; };
}; };
JsonProfile.prototype.addScriptSource = function (script, url, source) { JsonProfile.prototype.addScriptSource = function (scriptId, url, source) {
this.scripts_[script] = { this.scripts_[scriptId] = {
scriptId: scriptId,
name: url, name: url,
source: source source: source
}; };
......
...@@ -8,6 +8,7 @@ class State { ...@@ -8,6 +8,7 @@ class State {
#ic; #ic;
#selectedMapLogEvents; #selectedMapLogEvents;
#selectedIcLogEvents; #selectedIcLogEvents;
#selectedSourcePositionLogEvents;
#nofChunks; #nofChunks;
#chunks; #chunks;
#icTimeline; #icTimeline;
...@@ -81,6 +82,12 @@ class State { ...@@ -81,6 +82,12 @@ class State {
if (!value) return; if (!value) return;
this.#selectedMapLogEvents = value; this.#selectedMapLogEvents = value;
} }
get selectedSourcePositionLogEvents() {
return this.#selectedSourcePositionLogEvents;
}
set selectedSourcePositionLogEvents(value) {
this.#selectedSourcePositionLogEvents = value;
}
get selectedIcLogEvents() { get selectedIcLogEvents() {
return this.#selectedIcLogEvents; return this.#selectedIcLogEvents;
} }
......
...@@ -18,4 +18,14 @@ class Event { ...@@ -18,4 +18,14 @@ class Event {
} }
} }
export { Event }; class SourcePositionLogEvent extends Event {
constructor(type, time, file, line, col, script) {
super(type, time);
this.file = file;
this.line = line;
this.col = col;
this.script = script;
}
}
export { Event, SourcePositionLogEvent };
...@@ -5,12 +5,12 @@ ...@@ -5,12 +5,12 @@
import { Group } from './ic-model.mjs'; import { Group } from './ic-model.mjs';
import CustomIcProcessor from "./ic-processor.mjs"; import CustomIcProcessor from "./ic-processor.mjs";
import { MapLogEvent } from "./map-processor.mjs"; import { MapLogEvent } from "./map-processor.mjs";
import { SourcePositionLogEvent } from './event.mjs';
import { FocusEvent, SelectTimeEvent, SelectionEvent } from './events.mjs'; import { FocusEvent, SelectTimeEvent, SelectionEvent } from './events.mjs';
import { defineCustomElement, V8CustomElement } from './helper.mjs'; import { defineCustomElement, V8CustomElement } from './helper.mjs';
defineCustomElement('ic-panel', (templateText) => defineCustomElement('ic-panel', (templateText) =>
class ICPanel extends V8CustomElement { class ICPanel extends V8CustomElement {
//TODO(zcankara) Entries never set
#selectedLogEvents; #selectedLogEvents;
#timeline; #timeline;
constructor() { constructor() {
...@@ -29,8 +29,6 @@ defineCustomElement('ic-panel', (templateText) => ...@@ -29,8 +29,6 @@ defineCustomElement('ic-panel', (templateText) =>
this.selectedLogEvents = this.timeline.all; this.selectedLogEvents = this.timeline.all;
this.updateCount(); this.updateCount();
} }
get groupKey() { get groupKey() {
return this.$('#group-key'); return this.$('#group-key');
} }
...@@ -105,26 +103,38 @@ defineCustomElement('ic-panel', (templateText) => ...@@ -105,26 +103,38 @@ defineCustomElement('ic-panel', (templateText) =>
} }
handleMapClick(e) { handleMapClick(e) {
let entry = e.target.parentNode.entry; const entry = e.target.parentNode.entry;
let id = entry.key; const id = entry.key;
let selectedMapLofEvents = const selectedMapLogEvents =
this.searchIcLogEventToMapLogEvent(id, entry.entries); this.searchIcLogEventToMapLogEvent(id, entry.entries);
this.dispatchEvent(new SelectionEvent(selectedMapLofEvents)); this.dispatchEvent(new SelectionEvent(selectedMapLogEvents));
} }
searchIcLogEventToMapLogEvent(id, icLogEvents) { searchIcLogEventToMapLogEvent(id, icLogEvents) {
// searches for mapLogEvents using the id, time // searches for mapLogEvents using the id, time
let selectedMapLogEventsSet = new Set(); const selectedMapLogEventsSet = new Set();
for (const icLogEvent of icLogEvents) { for (const icLogEvent of icLogEvents) {
let time = icLogEvent.time; const time = icLogEvent.time;
let selectedMap = MapLogEvent.get(id, time); const selectedMap = MapLogEvent.get(id, time);
selectedMapLogEventsSet.add(selectedMap); selectedMapLogEventsSet.add(selectedMap);
} }
return Array.from(selectedMapLogEventsSet); return Array.from(selectedMapLogEventsSet);
} }
handleFilePositionClick(e) { handleFilePositionClick(e) {
this.dispatchEvent(new FocusEvent(e.target.parentNode.entry.key)); const entry = e.target.parentNode.entry;
const filePosition =
this.createSourcePositionLogEvent(
entry.entries[0].type, entry.entries[0].time, entry.key,
entry.entries[0].script);
this.dispatchEvent(new FocusEvent(filePosition));
}
createSourcePositionLogEvent(type, time, filePositionLine, script) {
const [file, line, col] = filePositionLine.split(':');
const filePosition = new SourcePositionLogEvent(type, time,
file, line, col, script);
return filePosition
} }
render(entries, parent) { render(entries, parent) {
......
...@@ -44,6 +44,10 @@ class IcProcessor extends LogReader { ...@@ -44,6 +44,10 @@ class IcProcessor extends LogReader {
], ],
processor: this.processV8Version processor: this.processV8Version
}, },
'script-source': {
parsers: [parseInt, parseString, parseString],
processor: this.processScriptSource
},
'code-move': 'code-move':
{ parsers: [parseInt, parseInt], processor: this.processCodeMove }, { parsers: [parseInt, parseInt], processor: this.processCodeMove },
'code-delete': { parsers: [parseInt], processor: this.processCodeDelete }, 'code-delete': { parsers: [parseInt], processor: this.processCodeDelete },
...@@ -122,6 +126,9 @@ class IcProcessor extends LogReader { ...@@ -122,6 +126,9 @@ class IcProcessor extends LogReader {
`Please use the matching tool for given the V8 version.`); `Please use the matching tool for given the V8 version.`);
} }
} }
processScriptSource(scriptId, url, script) {
this.#profile.addScriptSource(scriptId, url, script);
}
processLogFile(fileName) { processLogFile(fileName) {
this.collectEntries = true; this.collectEntries = true;
this.lastLogFileName_ = fileName; this.lastLogFileName_ = fileName;
...@@ -181,6 +188,11 @@ class IcProcessor extends LogReader { ...@@ -181,6 +188,11 @@ class IcProcessor extends LogReader {
' (map 0x' + map.toString(16) + ')' + ' (map 0x' + map.toString(16) + ')' +
(slow_reason ? ' ' + slow_reason : '') + 'time: ' + time); (slow_reason ? ' ' + slow_reason : '') + 'time: ' + time);
} }
getScript(url) {
return this.#profile.getScript(url);
}
} }
// ================ // ================
...@@ -209,9 +221,12 @@ class CustomIcProcessor extends IcProcessor { ...@@ -209,9 +221,12 @@ class CustomIcProcessor extends IcProcessor {
type, pc, time, line, column, old_state, new_state, map, key, modifier, type, pc, time, line, column, old_state, new_state, map, key, modifier,
slow_reason) { slow_reason) {
let fnName = this.functionName(pc); let fnName = this.functionName(pc);
let parts = fnName.split(' ');
let fileName = parts[1];
let script = this.getScript(fileName);
let entry = new IcLogEvent( let entry = new IcLogEvent(
type, fnName, time, line, column, key, old_state, new_state, map, type, fnName, time, line, column, key, old_state, new_state, map,
slow_reason); slow_reason, script);
this.#timeline.push(entry); this.#timeline.push(entry);
} }
...@@ -229,7 +244,7 @@ class CustomIcProcessor extends IcProcessor { ...@@ -229,7 +244,7 @@ class CustomIcProcessor extends IcProcessor {
class IcLogEvent extends Event { class IcLogEvent extends Event {
constructor( constructor(
type, fn_file, time, line, column, key, oldState, newState, map, reason, type, fn_file, time, line, column, key, oldState, newState, map, reason,
additional) { script, additional) {
super(type, time); super(type, time);
this.category = 'other'; this.category = 'other';
if (this.type.indexOf('Store') !== -1) { if (this.type.indexOf('Store') !== -1) {
...@@ -249,6 +264,7 @@ class IcLogEvent extends Event { ...@@ -249,6 +264,7 @@ class IcLogEvent extends Event {
this.map = map; this.map = map;
this.reason = reason; this.reason = reason;
this.additional = additional; this.additional = additional;
this.script = script;
} }
......
...@@ -135,6 +135,10 @@ button { ...@@ -135,6 +135,10 @@ button {
background-color: var(--error-color); background-color: var(--error-color);
} }
.highlight {
background-color: var(--primary-color);
color: var(--on-primary-color);
}
.clickable:hover, .clickable:hover,
.clickable:active { .clickable:active {
background-color: var(--primary-color); background-color: var(--primary-color);
......
...@@ -115,9 +115,9 @@ found in the LICENSE file. --> ...@@ -115,9 +115,9 @@ found in the LICENSE file. -->
<script type="module"> <script type="module">
import { App } from './index.mjs'; import { App } from './index.mjs';
globalThis.app = globalThis.app = new App("#log-file-reader", "#map-panel",
new App("#log-file-reader", "#map-panel", "#timeline-panel", "#timeline-panel", "#ic-panel", "#map-track", "#ic-track",
"#ic-panel", "#map-track", "#ic-track"); "#source-panel");
</script> </script>
</head> </head>
...@@ -141,6 +141,7 @@ found in the LICENSE file. --> ...@@ -141,6 +141,7 @@ found in the LICENSE file. -->
</timeline-panel> </timeline-panel>
<map-panel id="map-panel"></map-panel> <map-panel id="map-panel"></map-panel>
<ic-panel id="ic-panel" onchange="app.handleSelectIc(event)"></ic-panel> <ic-panel id="ic-panel" onchange="app.handleSelectIc(event)"></ic-panel>
<source-panel id="source-panel"></source-panel>
</div> </div>
</div> </div>
<div id="instructions"> <div id="instructions">
......
...@@ -8,17 +8,19 @@ import { IcLogEvent } from "./ic-processor.mjs"; ...@@ -8,17 +8,19 @@ import { IcLogEvent } from "./ic-processor.mjs";
import { State } from "./app-model.mjs"; import { State } from "./app-model.mjs";
import { MapProcessor, MapLogEvent } from "./map-processor.mjs"; import { MapProcessor, MapLogEvent } from "./map-processor.mjs";
import { SelectTimeEvent } from "./events.mjs"; import { SelectTimeEvent } from "./events.mjs";
import { SourcePositionLogEvent } from "./event.mjs";
import { $ } from "./helper.mjs"; import { $ } from "./helper.mjs";
import "./ic-panel.mjs"; import "./ic-panel.mjs";
import "./timeline-panel.mjs"; import "./timeline-panel.mjs";
import "./map-panel.mjs"; import "./map-panel.mjs";
import "./log-file-reader.mjs"; import "./log-file-reader.mjs";
import "./source-panel.mjs";
class App { class App {
#state; #state;
#view; #view;
#navigation; #navigation;
constructor(fileReaderId, mapPanelId, timelinePanelId, constructor(fileReaderId, mapPanelId, timelinePanelId,
icPanelId, mapTrackId, icTrackId) { icPanelId, mapTrackId, icTrackId, sourcePanelId) {
this.#view = { this.#view = {
logFileReader: $(fileReaderId), logFileReader: $(fileReaderId),
icPanel: $(icPanelId), icPanel: $(icPanelId),
...@@ -26,6 +28,7 @@ class App { ...@@ -26,6 +28,7 @@ class App {
timelinePanel: $(timelinePanelId), timelinePanel: $(timelinePanelId),
mapTrack: $(mapTrackId), mapTrack: $(mapTrackId),
icTrack: $(icTrackId), icTrack: $(icTrackId),
sourcePanel: $(sourcePanelId)
}; };
this.#state = new State(); this.#state = new State();
this.#navigation = new Navigation(this.#state, this.#view); this.#navigation = new Navigation(this.#state, this.#view);
...@@ -53,6 +56,8 @@ class App { ...@@ -53,6 +56,8 @@ class App {
this.showMapEntries(e.entries); this.showMapEntries(e.entries);
} else if (e.entries[0] instanceof IcLogEvent) { } else if (e.entries[0] instanceof IcLogEvent) {
this.showIcEntries(e.entries); this.showIcEntries(e.entries);
} else if (e.entries[0] instanceof SourcePositionLogEvent) {
this.showSourcePositionEntries(e.entries);
} else { } else {
console.error("Undefined selection!"); console.error("Undefined selection!");
} }
...@@ -65,6 +70,10 @@ class App { ...@@ -65,6 +70,10 @@ class App {
this.#state.selectedIcLogEvents = entries; this.#state.selectedIcLogEvents = entries;
this.#view.icPanel.selectedLogEvents = this.#state.selectedIcLogEvents; this.#view.icPanel.selectedLogEvents = this.#state.selectedIcLogEvents;
} }
showSourcePositionEntries(entries) {
//TODO(zcankara) Handle multiple source position selection events
this.#view.sourcePanel.selectedSourcePositions = entries;
}
handleTimeRangeSelect(e) { handleTimeRangeSelect(e) {
this.selectTimeRange(e.start, e.end); this.selectTimeRange(e.start, e.end);
...@@ -74,16 +83,12 @@ class App { ...@@ -74,16 +83,12 @@ class App {
this.selectMapLogEvent(e.entry); this.selectMapLogEvent(e.entry);
} else if (e.entry instanceof IcLogEvent) { } else if (e.entry instanceof IcLogEvent) {
this.selectICLogEvent(e.entry); this.selectICLogEvent(e.entry);
} else if (typeof e.entry === 'string') { } else if (e.entry instanceof SourcePositionLogEvent) {
this.selectSourcePositionEvent(e.entry); this.selectSourcePositionEvent(e.entry);
} else { } else {
console.log("undefined"); console.log("undefined");
} }
} }
handleClickSourcePositions(e) {
//TODO(zcankara) Handle source position
console.log("Entry containing source position: ", e.entries);
}
selectTimeRange(start, end) { selectTimeRange(start, end) {
this.#state.timeSelection.start = start; this.#state.timeSelection.start = start;
this.#state.timeSelection.end = end; this.#state.timeSelection.end = end;
...@@ -103,8 +108,11 @@ class App { ...@@ -103,8 +108,11 @@ class App {
this.#view.icPanel.selectedLogEvents = [entry]; this.#view.icPanel.selectedLogEvents = [entry];
} }
selectSourcePositionEvent(sourcePositions) { selectSourcePositionEvent(sourcePositions) {
if (!sourcePositions.script) return;
console.log("source positions: ", sourcePositions); console.log("source positions: ", sourcePositions);
this.#view.sourcePanel.selectedSourcePositions = [sourcePositions];
} }
handleFileUpload(e) { handleFileUpload(e) {
this.restartApp(); this.restartApp();
$("#container").className = "initial"; $("#container").className = "initial";
......
...@@ -6,7 +6,8 @@ found in the LICENSE file. --> ...@@ -6,7 +6,8 @@ found in the LICENSE file. -->
<link href="./index.css" rel="stylesheet"> <link href="./index.css" rel="stylesheet">
</head> </head>
<style> <style>
#mapDetails { #mapDetails,
#filePositionNode {
overflow-x: scroll; overflow-x: scroll;
} }
...@@ -17,5 +18,6 @@ found in the LICENSE file. --> ...@@ -17,5 +18,6 @@ found in the LICENSE file. -->
</style> </style>
<div class="panel"> <div class="panel">
<h4>Map Details</h4> <h4>Map Details</h4>
<section id="filePositionNode"></section>
<section id="mapDetails"></section> <section id="mapDetails"></section>
</div> </div>
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import { V8CustomElement, defineCustomElement } from "../helper.mjs"; import { V8CustomElement, defineCustomElement } from "../helper.mjs";
import { FocusEvent } from "../events.mjs"; import { FocusEvent } from "../events.mjs";
import { SourcePositionLogEvent } from '../event.mjs';
defineCustomElement( defineCustomElement(
"./map-panel/map-details", "./map-panel/map-details",
...@@ -10,8 +11,8 @@ defineCustomElement( ...@@ -10,8 +11,8 @@ defineCustomElement(
class MapDetails extends V8CustomElement { class MapDetails extends V8CustomElement {
constructor() { constructor() {
super(templateText); super(templateText);
this.mapDetails.addEventListener("click", () => this.#filePositionNode.addEventListener("click", e =>
this.handleClickSourcePositions() this.handleFilePositionClick(e)
); );
this.selectedMap = undefined; this.selectedMap = undefined;
} }
...@@ -19,23 +20,44 @@ defineCustomElement( ...@@ -19,23 +20,44 @@ defineCustomElement(
return this.$("#mapDetails"); return this.$("#mapDetails");
} }
get #filePositionNode() {
return this.$("#filePositionNode");
}
setSelectedMap(value) { setSelectedMap(value) {
this.selectedMap = value; this.selectedMap = value;
} }
set mapDetails(map) { set mapDetails(map) {
let details = ""; let details = "";
let clickableDetails = "";
if (map) { if (map) {
details += "ID: " + map.id; clickableDetails += "ID: " + map.id;
details += "\nSource location: " + map.filePosition; clickableDetails += "\nSource location: " + map.filePosition;
details += "\n" + map.description; details += "\n" + map.description;
this.setSelectedMap(map); this.setSelectedMap(map);
} }
this.#filePositionNode.innerText = clickableDetails;
this.#filePositionNode.classList.add("clickable");
this.mapDetails.innerText = details; this.mapDetails.innerText = details;
} }
handleClickSourcePositions() { handleFilePositionClick() {
this.dispatchEvent(new FocusEvent(this.selectedMap.filePosition)); let filePosition =
this.createSourcePositionLogEvent(
this.selectedMap.type, this.selectedMap.time,
this.selectedMap.filePosition, this.selectedMap.script);
this.dispatchEvent(new FocusEvent(filePosition));
}
createSourcePositionLogEvent(type, time, filePositionLine, script) {
// remove token
if (!(/\s/.test(filePositionLine))) return;
filePositionLine = filePositionLine.split(' ');
let [file, line, col] = filePositionLine[1].split(':');
let filePosition = new SourcePositionLogEvent(type, time,
file, line, col, script);
return filePosition
} }
} }
); );
...@@ -54,6 +54,10 @@ class MapProcessor extends LogReader { ...@@ -54,6 +54,10 @@ class MapProcessor extends LogReader {
], ],
processor: this.processV8Version processor: this.processV8Version
}, },
'script-source': {
parsers: [parseInt, parseString, parseString],
processor: this.processScriptSource
},
'code-move': { 'code-move': {
parsers: [parseInt, parseInt], parsers: [parseInt, parseInt],
'sfi-move': 'sfi-move':
...@@ -184,6 +188,10 @@ class MapProcessor extends LogReader { ...@@ -184,6 +188,10 @@ class MapProcessor extends LogReader {
} }
} }
processScriptSource(scriptId, url, source) {
this.#profile.addScriptSource(scriptId, url, source);
}
processCodeMove(from, to) { processCodeMove(from, to) {
this.#profile.moveCode(from, to); this.#profile.moveCode(from, to);
} }
...@@ -211,6 +219,12 @@ class MapProcessor extends LogReader { ...@@ -211,6 +219,12 @@ class MapProcessor extends LogReader {
} }
return entry + ':' + line + ':' + column; return entry + ':' + line + ':' + column;
} }
processFileName(filePositionLine) {
if (!(/\s/.test(filePositionLine))) return;
filePositionLine = filePositionLine.split(' ');
let file = filePositionLine[1].split(':')[0];
return file;
}
processMap(type, time, from, to, pc, line, column, reason, name) { processMap(type, time, from, to, pc, line, column, reason, name) {
let time_ = parseInt(time); let time_ = parseInt(time);
...@@ -219,6 +233,8 @@ class MapProcessor extends LogReader { ...@@ -219,6 +233,8 @@ class MapProcessor extends LogReader {
let to_ = this.getExistingMap(to, time_); let to_ = this.getExistingMap(to, time_);
let edge = new Edge(type, name, reason, time, from_, to_); let edge = new Edge(type, name, reason, time, from_, to_);
to_.filePosition = this.formatPC(pc, line, column); to_.filePosition = this.formatPC(pc, line, column);
let fileName = this.processFileName(to_.filePosition);
to_.script = this.getScript(fileName);
edge.finishSetup(); edge.finishSetup();
} }
...@@ -254,6 +270,10 @@ class MapProcessor extends LogReader { ...@@ -254,6 +270,10 @@ class MapProcessor extends LogReader {
}; };
return map; return map;
} }
getScript(url) {
return this.#profile.getScript(url);
}
} }
// =========================================================================== // ===========================================================================
...@@ -268,6 +288,7 @@ class MapLogEvent extends Event { ...@@ -268,6 +288,7 @@ class MapLogEvent extends Event {
leftId = 0; leftId = 0;
rightId = 0; rightId = 0;
filePosition = ''; filePosition = '';
script = '';
id = -1; id = -1;
constructor(id, time) { constructor(id, time) {
if (!time) throw new Error('Invalid time'); if (!time) throw new Error('Invalid time');
......
<!-- Copyright 2020 the V8 project authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<style>
@import "./index.css";
pre.scriptNode {
white-space: pre-wrap;
}
pre.scriptNode:before {
counter-reset: listing;
}
pre.scriptNode code {
counter-increment: listing;
}
pre.scriptNode code::before {
content: counter(listing) ". ";
display: inline-block;
width: 4em;
padding-left: auto;
margin-left: auto;
text-align: left;
}
</style>
<div class="panel">
<h2>Source Panel</h2>
<div id="script">
<pre class="scripNode"></pre>
</div>
</div>
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { V8CustomElement, defineCustomElement } from "./helper.mjs";
defineCustomElement(
"source-panel",
(templateText) =>
class SourcePanel extends V8CustomElement {
#selectedSourcePositions;
constructor() {
super(templateText);
}
get script() {
return this.$('#script');
}
get scriptNode() {
return this.$('.scriptNode');
}
set script(script) {
this.renderSourcePanel(script);
}
set selectedSourcePositions(sourcePositions) {
this.#selectedSourcePositions = sourcePositions;
this.renderSourcePanelSelectedHighlight();
}
get selectedSourcePositions() {
return this.#selectedSourcePositions;
}
highlightSourcePosition(line, col, script) {
//TODO(zcankara) change setting source to support multiple files
this.script = script;
let codeNodes = this.scriptNode.children;
for (let index = 1; index <= codeNodes.length; index++) {
if (index != line) continue;
let lineText = codeNodes[index - 1].innerHTML;
for (let char = 1; char <= lineText.length; char++) {
if (char != col) continue;
let target = char - 1;
codeNodes[line - 1].innerHTML = lineText.slice(0, target) +
"<span class='highlight'> </span>" +
lineText.slice(target, lineText.length);
}
}
}
createScriptNode() {
let scriptNode = document.createElement("pre");
scriptNode.classList.add('scriptNode');
return scriptNode;
}
renderSourcePanel(source) {
let scriptNode = this.createScriptNode();
let sourceLines = source.split("\n");
for (let line = 1; line <= sourceLines.length; line++) {
let codeNode = document.createElement("code");
codeNode.classList.add("line" + line);
codeNode.innerHTML = sourceLines[line - 1] + "\n";
scriptNode.appendChild(codeNode);
}
let oldScriptNode = this.script.childNodes[1];
this.script.replaceChild(scriptNode, oldScriptNode);
}
renderSourcePanelSelectedHighlight() {
for (const sourcePosition of this.selectedSourcePositions) {
let line = sourcePosition.line;
let col = sourcePosition.col;
let script = sourcePosition.script;
this.highlightSourcePosition(line, col, script);
}
}
}
);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment