2020-08-28 14:41:56 +00:00
|
|
|
// 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.
|
2020-12-07 08:44:54 +00:00
|
|
|
import {groupBy} from '../helper.mjs';
|
2020-11-18 15:51:07 +00:00
|
|
|
|
2020-12-21 09:47:32 +00:00
|
|
|
import {SelectRelatedEvent, ToolTipEvent} from './events.mjs';
|
2020-11-03 08:01:33 +00:00
|
|
|
import {delay, DOM, formatBytes, V8CustomElement} from './helper.mjs';
|
|
|
|
|
2021-01-07 09:22:38 +00:00
|
|
|
DOM.defineCustomElement('view/script-panel',
|
2020-11-03 08:01:33 +00:00
|
|
|
(templateText) =>
|
|
|
|
class SourcePanel extends V8CustomElement {
|
|
|
|
_selectedSourcePositions = [];
|
2020-12-21 09:47:32 +00:00
|
|
|
_sourcePositionsToMarkNodes = [];
|
2020-11-03 08:01:33 +00:00
|
|
|
_scripts = [];
|
|
|
|
_script;
|
2020-11-16 13:19:25 +00:00
|
|
|
|
2020-11-03 08:01:33 +00:00
|
|
|
constructor() {
|
|
|
|
super(templateText);
|
|
|
|
this.scriptDropdown.addEventListener(
|
|
|
|
'change', e => this._handleSelectScript(e));
|
2021-01-07 09:22:38 +00:00
|
|
|
this.$('#selectedRelatedButton').onclick =
|
|
|
|
this._handleSelectRelated.bind(this);
|
2020-11-03 08:01:33 +00:00
|
|
|
}
|
2020-11-02 09:27:28 +00:00
|
|
|
|
2020-11-03 08:01:33 +00:00
|
|
|
get script() {
|
|
|
|
return this.$('#script');
|
|
|
|
}
|
2020-11-02 09:27:28 +00:00
|
|
|
|
2020-11-03 08:01:33 +00:00
|
|
|
get scriptNode() {
|
|
|
|
return this.$('.scriptNode');
|
|
|
|
}
|
2020-11-02 09:27:28 +00:00
|
|
|
|
2020-11-03 08:01:33 +00:00
|
|
|
set script(script) {
|
|
|
|
if (this._script === script) return;
|
|
|
|
this._script = script;
|
|
|
|
this._renderSourcePanel();
|
|
|
|
this._updateScriptDropdownSelection();
|
|
|
|
}
|
2020-11-02 09:27:28 +00:00
|
|
|
|
2020-11-03 08:01:33 +00:00
|
|
|
set selectedSourcePositions(sourcePositions) {
|
|
|
|
this._selectedSourcePositions = sourcePositions;
|
|
|
|
// TODO: highlight multiple scripts
|
|
|
|
this.script = sourcePositions[0]?.script;
|
|
|
|
this._focusSelectedMarkers();
|
|
|
|
}
|
2020-11-02 09:27:28 +00:00
|
|
|
|
2020-12-21 09:47:32 +00:00
|
|
|
set focusedSourcePositions(sourcePositions) {
|
|
|
|
this.selectedSourcePositions = sourcePositions;
|
|
|
|
}
|
|
|
|
|
|
|
|
set scripts(scripts) {
|
2020-11-03 08:01:33 +00:00
|
|
|
this._scripts = scripts;
|
|
|
|
this._initializeScriptDropdown();
|
|
|
|
}
|
2020-11-02 09:27:28 +00:00
|
|
|
|
2020-11-03 08:01:33 +00:00
|
|
|
get scriptDropdown() {
|
|
|
|
return this.$('#script-dropdown');
|
|
|
|
}
|
2020-11-02 09:27:28 +00:00
|
|
|
|
2020-11-03 08:01:33 +00:00
|
|
|
_initializeScriptDropdown() {
|
|
|
|
this._scripts.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
let select = this.scriptDropdown;
|
|
|
|
select.options.length = 0;
|
|
|
|
for (const script of this._scripts) {
|
|
|
|
const option = document.createElement('option');
|
|
|
|
const size = formatBytes(script.source.length);
|
|
|
|
option.text = `${script.name} (id=${script.id} size=${size})`;
|
|
|
|
option.script = script;
|
|
|
|
select.add(option);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_updateScriptDropdownSelection() {
|
|
|
|
this.scriptDropdown.selectedIndex =
|
|
|
|
this._script ? this._scripts.indexOf(this._script) : -1;
|
|
|
|
}
|
2020-09-23 10:30:17 +00:00
|
|
|
|
2020-11-03 08:01:33 +00:00
|
|
|
async _renderSourcePanel() {
|
|
|
|
let scriptNode;
|
|
|
|
if (this._script) {
|
|
|
|
await delay(1);
|
2020-12-21 09:47:32 +00:00
|
|
|
const builder = new LineBuilder(this, this._script);
|
2020-11-03 08:01:33 +00:00
|
|
|
scriptNode = builder.createScriptNode();
|
|
|
|
this._sourcePositionsToMarkNodes = builder.sourcePositionToMarkers;
|
|
|
|
} else {
|
2020-11-26 17:02:36 +00:00
|
|
|
scriptNode = DOM.div();
|
2020-11-03 08:01:33 +00:00
|
|
|
this._selectedMarkNodes = undefined;
|
2020-12-21 09:47:32 +00:00
|
|
|
this._sourcePositionsToMarkNodes = new Map();
|
2020-11-03 08:01:33 +00:00
|
|
|
}
|
|
|
|
const oldScriptNode = this.script.childNodes[1];
|
|
|
|
this.script.replaceChild(scriptNode, oldScriptNode);
|
|
|
|
}
|
2020-09-23 10:30:17 +00:00
|
|
|
|
2020-11-03 08:01:33 +00:00
|
|
|
async _focusSelectedMarkers() {
|
|
|
|
await delay(100);
|
|
|
|
// Remove all marked nodes.
|
|
|
|
for (let markNode of this._sourcePositionsToMarkNodes.values()) {
|
|
|
|
markNode.className = '';
|
|
|
|
}
|
|
|
|
for (let sourcePosition of this._selectedSourcePositions) {
|
2020-12-21 09:47:32 +00:00
|
|
|
if (sourcePosition.script !== this._script) continue;
|
2020-11-03 08:01:33 +00:00
|
|
|
this._sourcePositionsToMarkNodes.get(sourcePosition).className = 'marked';
|
|
|
|
}
|
2020-12-21 09:47:32 +00:00
|
|
|
this._scrollToFirstSourcePosition()
|
|
|
|
}
|
|
|
|
|
|
|
|
_scrollToFirstSourcePosition() {
|
|
|
|
const sourcePosition = this._selectedSourcePositions.find(
|
|
|
|
each => each.script === this._script);
|
2020-11-03 08:01:33 +00:00
|
|
|
if (!sourcePosition) return;
|
|
|
|
const markNode = this._sourcePositionsToMarkNodes.get(sourcePosition);
|
|
|
|
markNode.scrollIntoView(
|
|
|
|
{behavior: 'smooth', block: 'nearest', inline: 'center'});
|
|
|
|
}
|
2020-11-02 09:27:28 +00:00
|
|
|
|
2020-11-03 08:01:33 +00:00
|
|
|
_handleSelectScript(e) {
|
|
|
|
const option =
|
|
|
|
this.scriptDropdown.options[this.scriptDropdown.selectedIndex];
|
|
|
|
this.script = option.script;
|
|
|
|
}
|
2020-09-23 10:30:17 +00:00
|
|
|
|
2021-01-07 09:22:38 +00:00
|
|
|
_handleSelectRelated(e) {
|
|
|
|
if (!this._script) return;
|
|
|
|
this.dispatchEvent(new SelectRelatedEvent(this._script));
|
|
|
|
}
|
|
|
|
|
2020-11-03 08:01:33 +00:00
|
|
|
handleSourcePositionClick(e) {
|
2020-11-27 13:12:53 +00:00
|
|
|
const sourcePosition = e.target.sourcePosition;
|
2020-12-21 09:47:32 +00:00
|
|
|
this.dispatchEvent(new SelectRelatedEvent(sourcePosition));
|
2020-11-03 08:01:33 +00:00
|
|
|
}
|
2020-11-27 13:12:53 +00:00
|
|
|
|
2020-11-26 14:07:19 +00:00
|
|
|
handleSourcePositionMouseOver(e) {
|
|
|
|
const entries = e.target.sourcePosition.entries;
|
2020-12-07 08:44:54 +00:00
|
|
|
let text = groupBy(entries, each => each.constructor, true)
|
|
|
|
.map(group => {
|
|
|
|
let text = `${group.key.name}: ${group.count}\n`
|
|
|
|
text += groupBy(group.entries, each => each.type, true)
|
|
|
|
.map(group => {
|
|
|
|
return ` - ${group.key}: ${group.count}`;
|
|
|
|
})
|
|
|
|
.join('\n');
|
|
|
|
return text;
|
|
|
|
})
|
|
|
|
.join('\n');
|
|
|
|
this.dispatchEvent(new ToolTipEvent(text, e.target));
|
2020-11-26 14:07:19 +00:00
|
|
|
}
|
2020-11-03 08:01:33 +00:00
|
|
|
});
|
2020-09-23 10:30:17 +00:00
|
|
|
|
|
|
|
class SourcePositionIterator {
|
2020-10-19 10:45:42 +00:00
|
|
|
_entries;
|
|
|
|
_index = 0;
|
2020-09-23 10:30:17 +00:00
|
|
|
constructor(sourcePositions) {
|
2020-10-19 10:45:42 +00:00
|
|
|
this._entries = sourcePositions;
|
2020-09-23 10:30:17 +00:00
|
|
|
}
|
|
|
|
|
2020-11-03 08:01:33 +00:00
|
|
|
* forLine(lineIndex) {
|
2020-11-02 09:27:28 +00:00
|
|
|
this._findStart(lineIndex);
|
2020-11-03 08:01:33 +00:00
|
|
|
while (!this._done() && this._current().line === lineIndex) {
|
2020-10-19 10:45:42 +00:00
|
|
|
yield this._current();
|
|
|
|
this._next();
|
2020-09-23 10:30:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-02 09:27:28 +00:00
|
|
|
_findStart(lineIndex) {
|
2020-11-03 08:01:33 +00:00
|
|
|
while (!this._done() && this._current().line < lineIndex) {
|
2020-11-02 09:27:28 +00:00
|
|
|
this._next();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-19 10:45:42 +00:00
|
|
|
_current() {
|
|
|
|
return this._entries[this._index];
|
2020-09-23 10:30:17 +00:00
|
|
|
}
|
|
|
|
|
2020-10-19 10:45:42 +00:00
|
|
|
_done() {
|
|
|
|
return this._index + 1 >= this._entries.length;
|
2020-09-23 10:30:17 +00:00
|
|
|
}
|
|
|
|
|
2020-10-19 10:45:42 +00:00
|
|
|
_next() {
|
|
|
|
this._index++;
|
2020-09-23 10:30:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-03 08:01:33 +00:00
|
|
|
function* lineIterator(source) {
|
2020-09-23 10:30:17 +00:00
|
|
|
let current = 0;
|
|
|
|
let line = 1;
|
2020-11-03 08:01:33 +00:00
|
|
|
while (current < source.length) {
|
|
|
|
const next = source.indexOf('\n', current);
|
2020-09-23 10:30:17 +00:00
|
|
|
if (next === -1) break;
|
|
|
|
yield [line, source.substring(current, next)];
|
|
|
|
line++;
|
|
|
|
current = next + 1;
|
|
|
|
}
|
|
|
|
if (current < source.length) yield [line, source.substring(current)];
|
|
|
|
}
|
|
|
|
|
|
|
|
class LineBuilder {
|
2020-11-02 09:27:28 +00:00
|
|
|
_script;
|
|
|
|
_clickHandler;
|
2020-11-26 14:07:19 +00:00
|
|
|
_mouseoverHandler;
|
2020-11-02 09:27:28 +00:00
|
|
|
_sourcePositions;
|
|
|
|
_sourcePositionToMarkers = new Map();
|
2020-09-23 10:30:17 +00:00
|
|
|
|
2020-12-21 09:47:32 +00:00
|
|
|
constructor(panel, script) {
|
2020-10-19 10:45:42 +00:00
|
|
|
this._script = script;
|
|
|
|
this._clickHandler = panel.handleSourcePositionClick.bind(panel);
|
2020-11-26 14:07:19 +00:00
|
|
|
this._mouseoverHandler = panel.handleSourcePositionMouseOver.bind(panel);
|
2020-09-23 10:30:17 +00:00
|
|
|
// TODO: sort on script finalization.
|
2020-11-10 11:47:40 +00:00
|
|
|
script.sourcePositions.sort((a, b) => {
|
|
|
|
if (a.line === b.line) return a.column - b.column;
|
|
|
|
return a.line - b.line;
|
|
|
|
});
|
|
|
|
this._sourcePositions = new SourcePositionIterator(script.sourcePositions);
|
2020-11-02 09:27:28 +00:00
|
|
|
}
|
2020-09-23 10:30:17 +00:00
|
|
|
|
2020-11-02 09:27:28 +00:00
|
|
|
get sourcePositionToMarkers() {
|
|
|
|
return this._sourcePositionToMarkers;
|
2020-09-23 10:30:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
createScriptNode() {
|
2020-11-26 17:02:36 +00:00
|
|
|
const scriptNode = DOM.div('scriptNode');
|
2020-10-19 10:45:42 +00:00
|
|
|
for (let [lineIndex, line] of lineIterator(this._script.source)) {
|
|
|
|
scriptNode.appendChild(this._createLineNode(lineIndex, line));
|
2020-09-23 10:30:17 +00:00
|
|
|
}
|
|
|
|
return scriptNode;
|
|
|
|
}
|
|
|
|
|
2020-10-19 10:45:42 +00:00
|
|
|
_createLineNode(lineIndex, line) {
|
2020-11-26 17:02:36 +00:00
|
|
|
const lineNode = DOM.span();
|
2020-11-03 08:01:33 +00:00
|
|
|
let columnIndex = 0;
|
2020-10-19 10:45:42 +00:00
|
|
|
for (const sourcePosition of this._sourcePositions.forLine(lineIndex)) {
|
2020-09-23 10:30:17 +00:00
|
|
|
const nextColumnIndex = sourcePosition.column - 1;
|
2020-11-03 08:01:33 +00:00
|
|
|
lineNode.appendChild(document.createTextNode(
|
|
|
|
line.substring(columnIndex, nextColumnIndex)));
|
2020-09-23 10:30:17 +00:00
|
|
|
columnIndex = nextColumnIndex;
|
|
|
|
|
|
|
|
lineNode.appendChild(
|
2020-10-19 10:45:42 +00:00
|
|
|
this._createMarkerNode(line[columnIndex], sourcePosition));
|
2020-09-23 10:30:17 +00:00
|
|
|
columnIndex++;
|
|
|
|
}
|
|
|
|
lineNode.appendChild(
|
2020-11-03 08:01:33 +00:00
|
|
|
document.createTextNode(line.substring(columnIndex) + '\n'));
|
2020-09-23 10:30:17 +00:00
|
|
|
return lineNode;
|
|
|
|
}
|
|
|
|
|
2020-10-19 10:45:42 +00:00
|
|
|
_createMarkerNode(text, sourcePosition) {
|
2020-11-03 08:01:33 +00:00
|
|
|
const marker = document.createElement('mark');
|
2020-11-02 09:27:28 +00:00
|
|
|
this._sourcePositionToMarkers.set(sourcePosition, marker);
|
2020-09-23 10:30:17 +00:00
|
|
|
marker.textContent = text;
|
|
|
|
marker.sourcePosition = sourcePosition;
|
2020-10-19 10:45:42 +00:00
|
|
|
marker.onclick = this._clickHandler;
|
2020-11-26 14:07:19 +00:00
|
|
|
marker.onmouseover = this._mouseoverHandler;
|
2020-09-23 10:30:17 +00:00
|
|
|
return marker;
|
|
|
|
}
|
2020-11-30 18:38:13 +00:00
|
|
|
}
|