[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: Sathya Gunasekaran <gsathya@chromium.org> Cr-Commit-Position: refs/heads/master@{#69610}
This commit is contained in:
parent
1512f89328
commit
0f6afbe125
@ -38,6 +38,8 @@ function Profile() {
|
||||
this.bottomUpTree_ = new CallTree();
|
||||
this.c_entries_ = {};
|
||||
this.ticks_ = [];
|
||||
this.scripts_ = [];
|
||||
this.urlToScript_ = new Map();
|
||||
};
|
||||
|
||||
|
||||
@ -226,8 +228,21 @@ Profile.prototype.addSourcePositions = function (
|
||||
/**
|
||||
* Adds script source code.
|
||||
*/
|
||||
Profile.prototype.addScriptSource = function (script, source) {
|
||||
// CLI does not need source code => ignore.
|
||||
Profile.prototype.addScriptSource = function (scriptId, url, source) {
|
||||
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 (
|
||||
};
|
||||
};
|
||||
|
||||
JsonProfile.prototype.addScriptSource = function (script, url, source) {
|
||||
this.scripts_[script] = {
|
||||
JsonProfile.prototype.addScriptSource = function (scriptId, url, source) {
|
||||
this.scripts_[scriptId] = {
|
||||
scriptId: scriptId,
|
||||
name: url,
|
||||
source: source
|
||||
};
|
||||
|
@ -8,6 +8,7 @@ class State {
|
||||
#ic;
|
||||
#selectedMapLogEvents;
|
||||
#selectedIcLogEvents;
|
||||
#selectedSourcePositionLogEvents;
|
||||
#nofChunks;
|
||||
#chunks;
|
||||
#icTimeline;
|
||||
@ -81,6 +82,12 @@ class State {
|
||||
if (!value) return;
|
||||
this.#selectedMapLogEvents = value;
|
||||
}
|
||||
get selectedSourcePositionLogEvents() {
|
||||
return this.#selectedSourcePositionLogEvents;
|
||||
}
|
||||
set selectedSourcePositionLogEvents(value) {
|
||||
this.#selectedSourcePositionLogEvents = value;
|
||||
}
|
||||
get selectedIcLogEvents() {
|
||||
return this.#selectedIcLogEvents;
|
||||
}
|
||||
|
@ -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 @@
|
||||
import { Group } from './ic-model.mjs';
|
||||
import CustomIcProcessor from "./ic-processor.mjs";
|
||||
import { MapLogEvent } from "./map-processor.mjs";
|
||||
import { SourcePositionLogEvent } from './event.mjs';
|
||||
import { FocusEvent, SelectTimeEvent, SelectionEvent } from './events.mjs';
|
||||
import { defineCustomElement, V8CustomElement } from './helper.mjs';
|
||||
|
||||
defineCustomElement('ic-panel', (templateText) =>
|
||||
class ICPanel extends V8CustomElement {
|
||||
//TODO(zcankara) Entries never set
|
||||
#selectedLogEvents;
|
||||
#timeline;
|
||||
constructor() {
|
||||
@ -29,8 +29,6 @@ defineCustomElement('ic-panel', (templateText) =>
|
||||
this.selectedLogEvents = this.timeline.all;
|
||||
this.updateCount();
|
||||
}
|
||||
|
||||
|
||||
get groupKey() {
|
||||
return this.$('#group-key');
|
||||
}
|
||||
@ -105,26 +103,38 @@ defineCustomElement('ic-panel', (templateText) =>
|
||||
}
|
||||
|
||||
handleMapClick(e) {
|
||||
let entry = e.target.parentNode.entry;
|
||||
let id = entry.key;
|
||||
let selectedMapLofEvents =
|
||||
const entry = e.target.parentNode.entry;
|
||||
const id = entry.key;
|
||||
const selectedMapLogEvents =
|
||||
this.searchIcLogEventToMapLogEvent(id, entry.entries);
|
||||
this.dispatchEvent(new SelectionEvent(selectedMapLofEvents));
|
||||
this.dispatchEvent(new SelectionEvent(selectedMapLogEvents));
|
||||
}
|
||||
|
||||
searchIcLogEventToMapLogEvent(id, icLogEvents) {
|
||||
// searches for mapLogEvents using the id, time
|
||||
let selectedMapLogEventsSet = new Set();
|
||||
const selectedMapLogEventsSet = new Set();
|
||||
for (const icLogEvent of icLogEvents) {
|
||||
let time = icLogEvent.time;
|
||||
let selectedMap = MapLogEvent.get(id, time);
|
||||
const time = icLogEvent.time;
|
||||
const selectedMap = MapLogEvent.get(id, time);
|
||||
selectedMapLogEventsSet.add(selectedMap);
|
||||
}
|
||||
return Array.from(selectedMapLogEventsSet);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -44,6 +44,10 @@ class IcProcessor extends LogReader {
|
||||
],
|
||||
processor: this.processV8Version
|
||||
},
|
||||
'script-source': {
|
||||
parsers: [parseInt, parseString, parseString],
|
||||
processor: this.processScriptSource
|
||||
},
|
||||
'code-move':
|
||||
{ parsers: [parseInt, parseInt], processor: this.processCodeMove },
|
||||
'code-delete': { parsers: [parseInt], processor: this.processCodeDelete },
|
||||
@ -122,6 +126,9 @@ class IcProcessor extends LogReader {
|
||||
`Please use the matching tool for given the V8 version.`);
|
||||
}
|
||||
}
|
||||
processScriptSource(scriptId, url, script) {
|
||||
this.#profile.addScriptSource(scriptId, url, script);
|
||||
}
|
||||
processLogFile(fileName) {
|
||||
this.collectEntries = true;
|
||||
this.lastLogFileName_ = fileName;
|
||||
@ -181,6 +188,11 @@ class IcProcessor extends LogReader {
|
||||
' (map 0x' + map.toString(16) + ')' +
|
||||
(slow_reason ? ' ' + slow_reason : '') + 'time: ' + time);
|
||||
}
|
||||
|
||||
getScript(url) {
|
||||
return this.#profile.getScript(url);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ================
|
||||
@ -209,9 +221,12 @@ class CustomIcProcessor extends IcProcessor {
|
||||
type, pc, time, line, column, old_state, new_state, map, key, modifier,
|
||||
slow_reason) {
|
||||
let fnName = this.functionName(pc);
|
||||
let parts = fnName.split(' ');
|
||||
let fileName = parts[1];
|
||||
let script = this.getScript(fileName);
|
||||
let entry = new IcLogEvent(
|
||||
type, fnName, time, line, column, key, old_state, new_state, map,
|
||||
slow_reason);
|
||||
slow_reason, script);
|
||||
this.#timeline.push(entry);
|
||||
}
|
||||
|
||||
@ -229,7 +244,7 @@ class CustomIcProcessor extends IcProcessor {
|
||||
class IcLogEvent extends Event {
|
||||
constructor(
|
||||
type, fn_file, time, line, column, key, oldState, newState, map, reason,
|
||||
additional) {
|
||||
script, additional) {
|
||||
super(type, time);
|
||||
this.category = 'other';
|
||||
if (this.type.indexOf('Store') !== -1) {
|
||||
@ -249,6 +264,7 @@ class IcLogEvent extends Event {
|
||||
this.map = map;
|
||||
this.reason = reason;
|
||||
this.additional = additional;
|
||||
this.script = script;
|
||||
}
|
||||
|
||||
|
||||
|
@ -135,6 +135,10 @@ button {
|
||||
background-color: var(--error-color);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--on-primary-color);
|
||||
}
|
||||
.clickable:hover,
|
||||
.clickable:active {
|
||||
background-color: var(--primary-color);
|
||||
|
@ -115,9 +115,9 @@ found in the LICENSE file. -->
|
||||
<script type="module">
|
||||
import { App } from './index.mjs';
|
||||
|
||||
globalThis.app =
|
||||
new App("#log-file-reader", "#map-panel", "#timeline-panel",
|
||||
"#ic-panel", "#map-track", "#ic-track");
|
||||
globalThis.app = new App("#log-file-reader", "#map-panel",
|
||||
"#timeline-panel", "#ic-panel", "#map-track", "#ic-track",
|
||||
"#source-panel");
|
||||
</script>
|
||||
</head>
|
||||
|
||||
@ -141,6 +141,7 @@ found in the LICENSE file. -->
|
||||
</timeline-panel>
|
||||
<map-panel id="map-panel"></map-panel>
|
||||
<ic-panel id="ic-panel" onchange="app.handleSelectIc(event)"></ic-panel>
|
||||
<source-panel id="source-panel"></source-panel>
|
||||
</div>
|
||||
</div>
|
||||
<div id="instructions">
|
||||
|
@ -8,17 +8,19 @@ import { IcLogEvent } from "./ic-processor.mjs";
|
||||
import { State } from "./app-model.mjs";
|
||||
import { MapProcessor, MapLogEvent } from "./map-processor.mjs";
|
||||
import { SelectTimeEvent } from "./events.mjs";
|
||||
import { SourcePositionLogEvent } from "./event.mjs";
|
||||
import { $ } from "./helper.mjs";
|
||||
import "./ic-panel.mjs";
|
||||
import "./timeline-panel.mjs";
|
||||
import "./map-panel.mjs";
|
||||
import "./log-file-reader.mjs";
|
||||
import "./source-panel.mjs";
|
||||
class App {
|
||||
#state;
|
||||
#view;
|
||||
#navigation;
|
||||
constructor(fileReaderId, mapPanelId, timelinePanelId,
|
||||
icPanelId, mapTrackId, icTrackId) {
|
||||
icPanelId, mapTrackId, icTrackId, sourcePanelId) {
|
||||
this.#view = {
|
||||
logFileReader: $(fileReaderId),
|
||||
icPanel: $(icPanelId),
|
||||
@ -26,6 +28,7 @@ class App {
|
||||
timelinePanel: $(timelinePanelId),
|
||||
mapTrack: $(mapTrackId),
|
||||
icTrack: $(icTrackId),
|
||||
sourcePanel: $(sourcePanelId)
|
||||
};
|
||||
this.#state = new State();
|
||||
this.#navigation = new Navigation(this.#state, this.#view);
|
||||
@ -53,6 +56,8 @@ class App {
|
||||
this.showMapEntries(e.entries);
|
||||
} else if (e.entries[0] instanceof IcLogEvent) {
|
||||
this.showIcEntries(e.entries);
|
||||
} else if (e.entries[0] instanceof SourcePositionLogEvent) {
|
||||
this.showSourcePositionEntries(e.entries);
|
||||
} else {
|
||||
console.error("Undefined selection!");
|
||||
}
|
||||
@ -65,6 +70,10 @@ class App {
|
||||
this.#state.selectedIcLogEvents = entries;
|
||||
this.#view.icPanel.selectedLogEvents = this.#state.selectedIcLogEvents;
|
||||
}
|
||||
showSourcePositionEntries(entries) {
|
||||
//TODO(zcankara) Handle multiple source position selection events
|
||||
this.#view.sourcePanel.selectedSourcePositions = entries;
|
||||
}
|
||||
|
||||
handleTimeRangeSelect(e) {
|
||||
this.selectTimeRange(e.start, e.end);
|
||||
@ -74,16 +83,12 @@ class App {
|
||||
this.selectMapLogEvent(e.entry);
|
||||
} else if (e.entry instanceof IcLogEvent) {
|
||||
this.selectICLogEvent(e.entry);
|
||||
} else if (typeof e.entry === 'string') {
|
||||
} else if (e.entry instanceof SourcePositionLogEvent) {
|
||||
this.selectSourcePositionEvent(e.entry);
|
||||
} else {
|
||||
console.log("undefined");
|
||||
}
|
||||
}
|
||||
handleClickSourcePositions(e) {
|
||||
//TODO(zcankara) Handle source position
|
||||
console.log("Entry containing source position: ", e.entries);
|
||||
}
|
||||
selectTimeRange(start, end) {
|
||||
this.#state.timeSelection.start = start;
|
||||
this.#state.timeSelection.end = end;
|
||||
@ -103,8 +108,11 @@ class App {
|
||||
this.#view.icPanel.selectedLogEvents = [entry];
|
||||
}
|
||||
selectSourcePositionEvent(sourcePositions) {
|
||||
if (!sourcePositions.script) return;
|
||||
console.log("source positions: ", sourcePositions);
|
||||
this.#view.sourcePanel.selectedSourcePositions = [sourcePositions];
|
||||
}
|
||||
|
||||
handleFileUpload(e) {
|
||||
this.restartApp();
|
||||
$("#container").className = "initial";
|
||||
|
@ -6,7 +6,8 @@ found in the LICENSE file. -->
|
||||
<link href="./index.css" rel="stylesheet">
|
||||
</head>
|
||||
<style>
|
||||
#mapDetails {
|
||||
#mapDetails,
|
||||
#filePositionNode {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
@ -17,5 +18,6 @@ found in the LICENSE file. -->
|
||||
</style>
|
||||
<div class="panel">
|
||||
<h4>Map Details</h4>
|
||||
<section id="filePositionNode"></section>
|
||||
<section id="mapDetails"></section>
|
||||
</div>
|
||||
|
@ -3,6 +3,7 @@
|
||||
// found in the LICENSE file.
|
||||
import { V8CustomElement, defineCustomElement } from "../helper.mjs";
|
||||
import { FocusEvent } from "../events.mjs";
|
||||
import { SourcePositionLogEvent } from '../event.mjs';
|
||||
|
||||
defineCustomElement(
|
||||
"./map-panel/map-details",
|
||||
@ -10,8 +11,8 @@ defineCustomElement(
|
||||
class MapDetails extends V8CustomElement {
|
||||
constructor() {
|
||||
super(templateText);
|
||||
this.mapDetails.addEventListener("click", () =>
|
||||
this.handleClickSourcePositions()
|
||||
this.#filePositionNode.addEventListener("click", e =>
|
||||
this.handleFilePositionClick(e)
|
||||
);
|
||||
this.selectedMap = undefined;
|
||||
}
|
||||
@ -19,23 +20,44 @@ defineCustomElement(
|
||||
return this.$("#mapDetails");
|
||||
}
|
||||
|
||||
get #filePositionNode() {
|
||||
return this.$("#filePositionNode");
|
||||
}
|
||||
|
||||
setSelectedMap(value) {
|
||||
this.selectedMap = value;
|
||||
}
|
||||
|
||||
set mapDetails(map) {
|
||||
let details = "";
|
||||
let clickableDetails = "";
|
||||
if (map) {
|
||||
details += "ID: " + map.id;
|
||||
details += "\nSource location: " + map.filePosition;
|
||||
clickableDetails += "ID: " + map.id;
|
||||
clickableDetails += "\nSource location: " + map.filePosition;
|
||||
details += "\n" + map.description;
|
||||
this.setSelectedMap(map);
|
||||
}
|
||||
this.#filePositionNode.innerText = clickableDetails;
|
||||
this.#filePositionNode.classList.add("clickable");
|
||||
this.mapDetails.innerText = details;
|
||||
}
|
||||
|
||||
handleClickSourcePositions() {
|
||||
this.dispatchEvent(new FocusEvent(this.selectedMap.filePosition));
|
||||
handleFilePositionClick() {
|
||||
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 {
|
||||
],
|
||||
processor: this.processV8Version
|
||||
},
|
||||
'script-source': {
|
||||
parsers: [parseInt, parseString, parseString],
|
||||
processor: this.processScriptSource
|
||||
},
|
||||
'code-move': {
|
||||
parsers: [parseInt, parseInt],
|
||||
'sfi-move':
|
||||
@ -184,6 +188,10 @@ class MapProcessor extends LogReader {
|
||||
}
|
||||
}
|
||||
|
||||
processScriptSource(scriptId, url, source) {
|
||||
this.#profile.addScriptSource(scriptId, url, source);
|
||||
}
|
||||
|
||||
processCodeMove(from, to) {
|
||||
this.#profile.moveCode(from, to);
|
||||
}
|
||||
@ -211,6 +219,12 @@ class MapProcessor extends LogReader {
|
||||
}
|
||||
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) {
|
||||
let time_ = parseInt(time);
|
||||
@ -219,6 +233,8 @@ class MapProcessor extends LogReader {
|
||||
let to_ = this.getExistingMap(to, time_);
|
||||
let edge = new Edge(type, name, reason, time, from_, to_);
|
||||
to_.filePosition = this.formatPC(pc, line, column);
|
||||
let fileName = this.processFileName(to_.filePosition);
|
||||
to_.script = this.getScript(fileName);
|
||||
edge.finishSetup();
|
||||
}
|
||||
|
||||
@ -254,6 +270,10 @@ class MapProcessor extends LogReader {
|
||||
};
|
||||
return map;
|
||||
}
|
||||
|
||||
getScript(url) {
|
||||
return this.#profile.getScript(url);
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
@ -268,6 +288,7 @@ class MapLogEvent extends Event {
|
||||
leftId = 0;
|
||||
rightId = 0;
|
||||
filePosition = '';
|
||||
script = '';
|
||||
id = -1;
|
||||
constructor(id, time) {
|
||||
if (!time) throw new Error('Invalid time');
|
||||
|
34
tools/system-analyzer/source-panel-template.html
Normal file
34
tools/system-analyzer/source-panel-template.html
Normal file
@ -0,0 +1,34 @@
|
||||
<!-- 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>
|
72
tools/system-analyzer/source-panel.mjs
Normal file
72
tools/system-analyzer/source-panel.mjs
Normal file
@ -0,0 +1,72 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
Loading…
Reference in New Issue
Block a user