Home | History | Annotate | Download | only in src
      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