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