1// Copyright 2015 the V8 project authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5interface PR { 6 prettyPrint(_: unknown, el: HTMLElement): void; 7} 8 9declare global { 10 const PR: PR; 11} 12 13import { Source, SourceResolver, sourcePositionToStringKey } from "../src/source-resolver"; 14import { SelectionBroker } from "../src/selection-broker"; 15import { View } from "../src/view"; 16import { MySelection } from "../src/selection"; 17import { ViewElements } from "../src/util"; 18import { SelectionHandler } from "./selection-handler"; 19 20export enum CodeMode { 21 MAIN_SOURCE = "main function", 22 INLINED_SOURCE = "inlined function" 23} 24 25export class CodeView extends View { 26 broker: SelectionBroker; 27 source: Source; 28 sourceResolver: SourceResolver; 29 codeMode: CodeMode; 30 sourcePositionToHtmlElements: Map<string, Array<HTMLElement>>; 31 showAdditionalInliningPosition: boolean; 32 selectionHandler: SelectionHandler; 33 selection: MySelection; 34 35 createViewElement() { 36 const sourceContainer = document.createElement("div"); 37 sourceContainer.classList.add("source-container"); 38 return sourceContainer; 39 } 40 41 constructor(parent: HTMLElement, broker: SelectionBroker, sourceResolver: SourceResolver, sourceFunction: Source, codeMode: CodeMode) { 42 super(parent); 43 const view = this; 44 view.broker = broker; 45 view.sourceResolver = sourceResolver; 46 view.source = sourceFunction; 47 view.codeMode = codeMode; 48 this.sourcePositionToHtmlElements = new Map(); 49 this.showAdditionalInliningPosition = false; 50 51 const selectionHandler = { 52 clear: function () { 53 view.selection.clear(); 54 view.updateSelection(); 55 broker.broadcastClear(this); 56 }, 57 select: function (sourcePositions, selected) { 58 const locations = []; 59 for (const sourcePosition of sourcePositions) { 60 locations.push(sourcePosition); 61 sourceResolver.addInliningPositions(sourcePosition, locations); 62 } 63 if (locations.length == 0) return; 64 view.selection.select(locations, selected); 65 view.updateSelection(); 66 broker.broadcastSourcePositionSelect(this, locations, selected); 67 }, 68 brokeredSourcePositionSelect: function (locations, selected) { 69 const firstSelect = view.selection.isEmpty(); 70 for (const location of locations) { 71 const translated = sourceResolver.translateToSourceId(view.source.sourceId, location); 72 if (!translated) continue; 73 view.selection.select([translated], selected); 74 } 75 view.updateSelection(firstSelect); 76 }, 77 brokeredClear: function () { 78 view.selection.clear(); 79 view.updateSelection(); 80 }, 81 }; 82 view.selection = new MySelection(sourcePositionToStringKey); 83 broker.addSourcePositionHandler(selectionHandler); 84 this.selectionHandler = selectionHandler; 85 this.initializeCode(); 86 } 87 88 addHtmlElementToSourcePosition(sourcePosition, element) { 89 const key = sourcePositionToStringKey(sourcePosition); 90 if (!this.sourcePositionToHtmlElements.has(key)) { 91 this.sourcePositionToHtmlElements.set(key, []); 92 } 93 this.sourcePositionToHtmlElements.get(key).push(element); 94 } 95 96 getHtmlElementForSourcePosition(sourcePosition) { 97 const key = sourcePositionToStringKey(sourcePosition); 98 return this.sourcePositionToHtmlElements.get(key); 99 } 100 101 updateSelection(scrollIntoView: boolean = false): void { 102 const mkVisible = new ViewElements(this.divNode.parentNode as HTMLElement); 103 for (const [sp, els] of this.sourcePositionToHtmlElements.entries()) { 104 const isSelected = this.selection.isKeySelected(sp); 105 for (const el of els) { 106 mkVisible.consider(el, isSelected); 107 el.classList.toggle("selected", isSelected); 108 } 109 } 110 mkVisible.apply(scrollIntoView); 111 } 112 113 getCodeHtmlElementName() { 114 return `source-pre-${this.source.sourceId}`; 115 } 116 117 getCodeHeaderHtmlElementName() { 118 return `source-pre-${this.source.sourceId}-header`; 119 } 120 121 getHtmlCodeLines(): NodeListOf<HTMLElement> { 122 const ordereList = this.divNode.querySelector(`#${this.getCodeHtmlElementName()} ol`); 123 return ordereList.childNodes as NodeListOf<HTMLElement>; 124 } 125 126 onSelectLine(lineNumber: number, doClear: boolean) { 127 if (doClear) { 128 this.selectionHandler.clear(); 129 } 130 const positions = this.sourceResolver.lineToSourcePositions(lineNumber - 1); 131 if (positions !== undefined) { 132 this.selectionHandler.select(positions, undefined); 133 } 134 } 135 136 onSelectSourcePosition(sourcePosition, doClear: boolean) { 137 if (doClear) { 138 this.selectionHandler.clear(); 139 } 140 this.selectionHandler.select([sourcePosition], undefined); 141 } 142 143 initializeCode() { 144 const view = this; 145 const source = this.source; 146 const sourceText = source.sourceText; 147 if (!sourceText) return; 148 const sourceContainer = view.divNode; 149 if (this.codeMode == CodeMode.MAIN_SOURCE) { 150 sourceContainer.classList.add("main-source"); 151 } else { 152 sourceContainer.classList.add("inlined-source"); 153 } 154 const codeHeader = document.createElement("div"); 155 codeHeader.setAttribute("id", this.getCodeHeaderHtmlElementName()); 156 codeHeader.classList.add("code-header"); 157 const codeFileFunction = document.createElement("div"); 158 codeFileFunction.classList.add("code-file-function"); 159 codeFileFunction.innerHTML = `${source.sourceName}:${source.functionName}`; 160 codeHeader.appendChild(codeFileFunction); 161 const codeModeDiv = document.createElement("div"); 162 codeModeDiv.classList.add("code-mode"); 163 codeModeDiv.innerHTML = `${this.codeMode}`; 164 codeHeader.appendChild(codeModeDiv); 165 const clearDiv = document.createElement("div"); 166 clearDiv.style.clear = "both"; 167 codeHeader.appendChild(clearDiv); 168 sourceContainer.appendChild(codeHeader); 169 const codePre = document.createElement("pre"); 170 codePre.setAttribute("id", this.getCodeHtmlElementName()); 171 codePre.classList.add("prettyprint"); 172 sourceContainer.appendChild(codePre); 173 174 codeHeader.onclick = function myFunction() { 175 if (codePre.style.display === "none") { 176 codePre.style.display = "block"; 177 } else { 178 codePre.style.display = "none"; 179 } 180 }; 181 if (sourceText != "") { 182 codePre.classList.add("linenums"); 183 codePre.textContent = sourceText; 184 try { 185 // Wrap in try to work when offline. 186 PR.prettyPrint(undefined, sourceContainer); 187 } catch (e) { 188 console.log(e); 189 } 190 191 view.divNode.onclick = function (e: MouseEvent) { 192 if (e.target instanceof Element && e.target.tagName == "DIV") { 193 const targetDiv = e.target as HTMLDivElement; 194 if (targetDiv.classList.contains("line-number")) { 195 e.stopPropagation(); 196 view.onSelectLine(Number(targetDiv.dataset.lineNumber), !e.shiftKey); 197 } 198 } else { 199 view.selectionHandler.clear(); 200 } 201 }; 202 203 const base: number = source.startPosition; 204 let current = 0; 205 const lineListDiv = this.getHtmlCodeLines(); 206 let newlineAdjust = 0; 207 for (let i = 0; i < lineListDiv.length; i++) { 208 // Line numbers are not zero-based. 209 const lineNumber = i + 1; 210 const currentLineElement = lineListDiv[i]; 211 currentLineElement.id = "li" + i; 212 currentLineElement.dataset.lineNumber = "" + lineNumber; 213 const spans = currentLineElement.childNodes; 214 for (const currentSpan of spans) { 215 if (currentSpan instanceof HTMLSpanElement) { 216 const pos = base + current; 217 const end = pos + currentSpan.textContent.length; 218 current += currentSpan.textContent.length; 219 this.insertSourcePositions(currentSpan, lineNumber, pos, end, newlineAdjust); 220 newlineAdjust = 0; 221 } 222 } 223 224 this.insertLineNumber(currentLineElement, lineNumber); 225 226 while ((current < sourceText.length) && 227 (sourceText[current] == '\n' || sourceText[current] == '\r')) { 228 ++current; 229 ++newlineAdjust; 230 } 231 } 232 } 233 } 234 235 insertSourcePositions(currentSpan, lineNumber, pos, end, adjust) { 236 const view = this; 237 const sps = this.sourceResolver.sourcePositionsInRange(this.source.sourceId, pos - adjust, end); 238 let offset = 0; 239 for (const sourcePosition of sps) { 240 // Internally, line numbers are 0-based so we have to substract 1 from the line number. This 241 // path in only taken by non-Wasm code. Wasm code relies on setSourceLineToBytecodePosition. 242 this.sourceResolver.addAnyPositionToLine(lineNumber - 1, sourcePosition); 243 const textnode = currentSpan.tagName == 'SPAN' ? currentSpan.lastChild : currentSpan; 244 if (!(textnode instanceof Text)) continue; 245 const splitLength = Math.max(0, sourcePosition.scriptOffset - pos - offset); 246 offset += splitLength; 247 const replacementNode = textnode.splitText(splitLength); 248 const span = document.createElement('span'); 249 span.setAttribute("scriptOffset", sourcePosition.scriptOffset); 250 span.classList.add("source-position"); 251 const marker = document.createElement('span'); 252 marker.classList.add("marker"); 253 span.appendChild(marker); 254 const inlining = this.sourceResolver.getInliningForPosition(sourcePosition); 255 if (inlining != undefined && view.showAdditionalInliningPosition) { 256 const sourceName = this.sourceResolver.getSourceName(inlining.sourceId); 257 const inliningMarker = document.createElement('span'); 258 inliningMarker.classList.add("inlining-marker"); 259 inliningMarker.setAttribute("data-descr", `${sourceName} was inlined here`); 260 span.appendChild(inliningMarker); 261 } 262 span.onclick = function (e) { 263 e.stopPropagation(); 264 view.onSelectSourcePosition(sourcePosition, !e.shiftKey); 265 }; 266 view.addHtmlElementToSourcePosition(sourcePosition, span); 267 textnode.parentNode.insertBefore(span, replacementNode); 268 } 269 } 270 271 insertLineNumber(lineElement: HTMLElement, lineNumber: number) { 272 const view = this; 273 const lineNumberElement = document.createElement("div"); 274 lineNumberElement.classList.add("line-number"); 275 lineNumberElement.dataset.lineNumber = `${lineNumber}`; 276 lineNumberElement.innerText = `${lineNumber}`; 277 lineElement.insertBefore(lineNumberElement, lineElement.firstChild); 278 for (const sourcePosition of this.sourceResolver.lineToSourcePositions(lineNumber - 1)) { 279 view.addHtmlElementToSourcePosition(sourcePosition, lineElement); 280 } 281 } 282 283} 284