• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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