|  | // Copyright 2015 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. | 
|  |  | 
|  | interface PR { | 
|  | prettyPrint(_: unknown, el: HTMLElement): void; | 
|  | } | 
|  |  | 
|  | declare global { | 
|  | const PR: PR; | 
|  | } | 
|  |  | 
|  | import { Source, SourceResolver, sourcePositionToStringKey } from "../src/source-resolver"; | 
|  | import { SelectionBroker } from "../src/selection-broker"; | 
|  | import { View } from "../src/view"; | 
|  | import { MySelection } from "../src/selection"; | 
|  | import { ViewElements } from "../src/util"; | 
|  | import { SelectionHandler } from "./selection-handler"; | 
|  |  | 
|  | export enum CodeMode { | 
|  | MAIN_SOURCE = "main function", | 
|  | INLINED_SOURCE = "inlined function" | 
|  | } | 
|  |  | 
|  | export class CodeView extends View { | 
|  | broker: SelectionBroker; | 
|  | source: Source; | 
|  | sourceResolver: SourceResolver; | 
|  | codeMode: CodeMode; | 
|  | sourcePositionToHtmlElement: Map<string, HTMLElement>; | 
|  | showAdditionalInliningPosition: boolean; | 
|  | selectionHandler: SelectionHandler; | 
|  | selection: MySelection; | 
|  |  | 
|  | createViewElement() { | 
|  | const sourceContainer = document.createElement("div"); | 
|  | sourceContainer.classList.add("source-container"); | 
|  | return sourceContainer; | 
|  | } | 
|  |  | 
|  | constructor(parent: HTMLElement, broker: SelectionBroker, sourceResolver: SourceResolver, sourceFunction: Source, codeMode: CodeMode) { | 
|  | super(parent); | 
|  | const view = this; | 
|  | view.broker = broker; | 
|  | view.sourceResolver = sourceResolver; | 
|  | view.source = sourceFunction; | 
|  | view.codeMode = codeMode; | 
|  | this.sourcePositionToHtmlElement = new Map(); | 
|  | this.showAdditionalInliningPosition = false; | 
|  |  | 
|  | const selectionHandler = { | 
|  | clear: function () { | 
|  | view.selection.clear(); | 
|  | view.updateSelection(); | 
|  | broker.broadcastClear(this); | 
|  | }, | 
|  | select: function (sourcePositions, selected) { | 
|  | const locations = []; | 
|  | for (const sourcePosition of sourcePositions) { | 
|  | locations.push(sourcePosition); | 
|  | sourceResolver.addInliningPositions(sourcePosition, locations); | 
|  | } | 
|  | if (locations.length == 0) return; | 
|  | view.selection.select(locations, selected); | 
|  | view.updateSelection(); | 
|  | broker.broadcastSourcePositionSelect(this, locations, selected); | 
|  | }, | 
|  | brokeredSourcePositionSelect: function (locations, selected) { | 
|  | const firstSelect = view.selection.isEmpty(); | 
|  | for (const location of locations) { | 
|  | const translated = sourceResolver.translateToSourceId(view.source.sourceId, location); | 
|  | if (!translated) continue; | 
|  | view.selection.select([translated], selected); | 
|  | } | 
|  | view.updateSelection(firstSelect); | 
|  | }, | 
|  | brokeredClear: function () { | 
|  | view.selection.clear(); | 
|  | view.updateSelection(); | 
|  | }, | 
|  | }; | 
|  | view.selection = new MySelection(sourcePositionToStringKey); | 
|  | broker.addSourcePositionHandler(selectionHandler); | 
|  | this.selectionHandler = selectionHandler; | 
|  | this.initializeCode(); | 
|  | } | 
|  |  | 
|  | addHtmlElementToSourcePosition(sourcePosition, element) { | 
|  | const key = sourcePositionToStringKey(sourcePosition); | 
|  | if (this.sourcePositionToHtmlElement.has(key)) { | 
|  | console.log("Warning: duplicate source position", sourcePosition); | 
|  | } | 
|  | this.sourcePositionToHtmlElement.set(key, element); | 
|  | } | 
|  |  | 
|  | getHtmlElementForSourcePosition(sourcePosition) { | 
|  | const key = sourcePositionToStringKey(sourcePosition); | 
|  | return this.sourcePositionToHtmlElement.get(key); | 
|  | } | 
|  |  | 
|  | updateSelection(scrollIntoView: boolean = false): void { | 
|  | const mkVisible = new ViewElements(this.divNode.parentNode as HTMLElement); | 
|  | for (const [sp, el] of this.sourcePositionToHtmlElement.entries()) { | 
|  | const isSelected = this.selection.isKeySelected(sp); | 
|  | mkVisible.consider(el, isSelected); | 
|  | el.classList.toggle("selected", isSelected); | 
|  | } | 
|  | mkVisible.apply(scrollIntoView); | 
|  | } | 
|  |  | 
|  | getCodeHtmlElementName() { | 
|  | return `source-pre-${this.source.sourceId}`; | 
|  | } | 
|  |  | 
|  | getCodeHeaderHtmlElementName() { | 
|  | return `source-pre-${this.source.sourceId}-header`; | 
|  | } | 
|  |  | 
|  | getHtmlCodeLines(): NodeListOf<HTMLElement> { | 
|  | const ordereList = this.divNode.querySelector(`#${this.getCodeHtmlElementName()} ol`); | 
|  | return ordereList.childNodes as NodeListOf<HTMLElement>; | 
|  | } | 
|  |  | 
|  | onSelectLine(lineNumber: number, doClear: boolean) { | 
|  | if (doClear) { | 
|  | this.selectionHandler.clear(); | 
|  | } | 
|  | const positions = this.sourceResolver.linetoSourcePositions(lineNumber - 1); | 
|  | if (positions !== undefined) { | 
|  | this.selectionHandler.select(positions, undefined); | 
|  | } | 
|  | } | 
|  |  | 
|  | onSelectSourcePosition(sourcePosition, doClear: boolean) { | 
|  | if (doClear) { | 
|  | this.selectionHandler.clear(); | 
|  | } | 
|  | this.selectionHandler.select([sourcePosition], undefined); | 
|  | } | 
|  |  | 
|  | initializeCode() { | 
|  | const view = this; | 
|  | const source = this.source; | 
|  | const sourceText = source.sourceText; | 
|  | if (!sourceText) return; | 
|  | const sourceContainer = view.divNode; | 
|  | if (this.codeMode == CodeMode.MAIN_SOURCE) { | 
|  | sourceContainer.classList.add("main-source"); | 
|  | } else { | 
|  | sourceContainer.classList.add("inlined-source"); | 
|  | } | 
|  | const codeHeader = document.createElement("div"); | 
|  | codeHeader.setAttribute("id", this.getCodeHeaderHtmlElementName()); | 
|  | codeHeader.classList.add("code-header"); | 
|  | const codeFileFunction = document.createElement("div"); | 
|  | codeFileFunction.classList.add("code-file-function"); | 
|  | codeFileFunction.innerHTML = `${source.sourceName}:${source.functionName}`; | 
|  | codeHeader.appendChild(codeFileFunction); | 
|  | const codeModeDiv = document.createElement("div"); | 
|  | codeModeDiv.classList.add("code-mode"); | 
|  | codeModeDiv.innerHTML = `${this.codeMode}`; | 
|  | codeHeader.appendChild(codeModeDiv); | 
|  | const clearDiv = document.createElement("div"); | 
|  | clearDiv.style.clear = "both"; | 
|  | codeHeader.appendChild(clearDiv); | 
|  | sourceContainer.appendChild(codeHeader); | 
|  | const codePre = document.createElement("pre"); | 
|  | codePre.setAttribute("id", this.getCodeHtmlElementName()); | 
|  | codePre.classList.add("prettyprint"); | 
|  | sourceContainer.appendChild(codePre); | 
|  |  | 
|  | codeHeader.onclick = function myFunction() { | 
|  | if (codePre.style.display === "none") { | 
|  | codePre.style.display = "block"; | 
|  | } else { | 
|  | codePre.style.display = "none"; | 
|  | } | 
|  | }; | 
|  | if (sourceText != "") { | 
|  | codePre.classList.add("linenums"); | 
|  | codePre.textContent = sourceText; | 
|  | try { | 
|  | // Wrap in try to work when offline. | 
|  | PR.prettyPrint(undefined, sourceContainer); | 
|  | } catch (e) { | 
|  | console.log(e); | 
|  | } | 
|  |  | 
|  | view.divNode.onclick = function (e: MouseEvent) { | 
|  | if (e.target instanceof Element && e.target.tagName == "DIV") { | 
|  | const targetDiv = e.target as HTMLDivElement; | 
|  | if (targetDiv.classList.contains("line-number")) { | 
|  | e.stopPropagation(); | 
|  | view.onSelectLine(Number(targetDiv.dataset.lineNumber), !e.shiftKey); | 
|  | } | 
|  | } else { | 
|  | view.selectionHandler.clear(); | 
|  | } | 
|  | }; | 
|  |  | 
|  | const base: number = source.startPosition; | 
|  | let current = 0; | 
|  | const lineListDiv = this.getHtmlCodeLines(); | 
|  | let newlineAdjust = 0; | 
|  | for (let i = 0; i < lineListDiv.length; i++) { | 
|  | // Line numbers are not zero-based. | 
|  | const lineNumber = i + 1; | 
|  | const currentLineElement = lineListDiv[i]; | 
|  | currentLineElement.id = "li" + i; | 
|  | currentLineElement.dataset.lineNumber = "" + lineNumber; | 
|  | const spans = currentLineElement.childNodes; | 
|  | for (const currentSpan of spans) { | 
|  | if (currentSpan instanceof HTMLSpanElement) { | 
|  | const pos = base + current; | 
|  | const end = pos + currentSpan.textContent.length; | 
|  | current += currentSpan.textContent.length; | 
|  | this.insertSourcePositions(currentSpan, lineNumber, pos, end, newlineAdjust); | 
|  | newlineAdjust = 0; | 
|  | } | 
|  | } | 
|  |  | 
|  | this.insertLineNumber(currentLineElement, lineNumber); | 
|  |  | 
|  | while ((current < sourceText.length) && | 
|  | (sourceText[current] == '\n' || sourceText[current] == '\r')) { | 
|  | ++current; | 
|  | ++newlineAdjust; | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | insertSourcePositions(currentSpan, lineNumber, pos, end, adjust) { | 
|  | const view = this; | 
|  | const sps = this.sourceResolver.sourcePositionsInRange(this.source.sourceId, pos - adjust, end); | 
|  | let offset = 0; | 
|  | for (const sourcePosition of sps) { | 
|  | this.sourceResolver.addAnyPositionToLine(lineNumber, sourcePosition); | 
|  | const textnode = currentSpan.tagName == 'SPAN' ? currentSpan.lastChild : currentSpan; | 
|  | if (!(textnode instanceof Text)) continue; | 
|  | const splitLength = Math.max(0, sourcePosition.scriptOffset - pos - offset); | 
|  | offset += splitLength; | 
|  | const replacementNode = textnode.splitText(splitLength); | 
|  | const span = document.createElement('span'); | 
|  | span.setAttribute("scriptOffset", sourcePosition.scriptOffset); | 
|  | span.classList.add("source-position"); | 
|  | const marker = document.createElement('span'); | 
|  | marker.classList.add("marker"); | 
|  | span.appendChild(marker); | 
|  | const inlining = this.sourceResolver.getInliningForPosition(sourcePosition); | 
|  | if (inlining != undefined && view.showAdditionalInliningPosition) { | 
|  | const sourceName = this.sourceResolver.getSourceName(inlining.sourceId); | 
|  | const inliningMarker = document.createElement('span'); | 
|  | inliningMarker.classList.add("inlining-marker"); | 
|  | inliningMarker.setAttribute("data-descr", `${sourceName} was inlined here`); | 
|  | span.appendChild(inliningMarker); | 
|  | } | 
|  | span.onclick = function (e) { | 
|  | e.stopPropagation(); | 
|  | view.onSelectSourcePosition(sourcePosition, !e.shiftKey); | 
|  | }; | 
|  | view.addHtmlElementToSourcePosition(sourcePosition, span); | 
|  | textnode.parentNode.insertBefore(span, replacementNode); | 
|  | } | 
|  | } | 
|  |  | 
|  | insertLineNumber(lineElement: HTMLElement, lineNumber: number) { | 
|  | const view = this; | 
|  | const lineNumberElement = document.createElement("div"); | 
|  | lineNumberElement.classList.add("line-number"); | 
|  | lineNumberElement.dataset.lineNumber = `${lineNumber}`; | 
|  | lineNumberElement.innerText = `${lineNumber}`; | 
|  | lineElement.insertBefore(lineNumberElement, lineElement.firstChild); | 
|  | // Don't add lines to source positions of not in backwardsCompatibility mode. | 
|  | if (this.source.backwardsCompatibility === true) { | 
|  | for (const sourcePosition of this.sourceResolver.linetoSourcePositions(lineNumber - 1)) { | 
|  | view.addHtmlElementToSourcePosition(sourcePosition, lineElement); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | } |