code-view.ts 10.3 KB
Newer Older
1 2 3 4
// 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.

5 6 7 8 9 10 11 12
interface PR {
  prettyPrint(_: unknown, el: HTMLElement): void;
}

declare global {
  const PR: PR;
}

13 14 15 16 17
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";
18
import { SelectionHandler } from "./selection-handler";
19

20
export enum CodeMode {
21 22
  MAIN_SOURCE = "main function",
  INLINED_SOURCE = "inlined function"
23
}
24

25
export class CodeView extends View {
26 27 28 29 30 31 32 33
  broker: SelectionBroker;
  source: Source;
  sourceResolver: SourceResolver;
  codeMode: CodeMode;
  sourcePositionToHtmlElement: Map<string, HTMLElement>;
  showAdditionalInliningPosition: boolean;
  selectionHandler: SelectionHandler;
  selection: MySelection;
34 35 36 37 38 39 40

  createViewElement() {
    const sourceContainer = document.createElement("div");
    sourceContainer.classList.add("source-container");
    return sourceContainer;
  }

41 42
  constructor(parent: HTMLElement, broker: SelectionBroker, sourceResolver: SourceResolver, sourceFunction: Source, codeMode: CodeMode) {
    super(parent);
43
    const view = this;
44
    view.broker = broker;
45 46 47 48 49 50 51 52 53 54
    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();
55
        broker.broadcastClear(this);
56
      },
57 58
      select: function (sourcePositions, selected) {
        const locations = [];
59
        for (const sourcePosition of sourcePositions) {
60 61
          locations.push(sourcePosition);
          sourceResolver.addInliningPositions(sourcePosition, locations);
62
        }
63 64 65 66
        if (locations.length == 0) return;
        view.selection.select(locations, selected);
        view.updateSelection();
        broker.broadcastSourcePositionSelect(this, locations, selected);
67
      },
68 69 70 71 72
      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;
73
          view.selection.select([translated], selected);
74
        }
75 76 77 78 79
        view.updateSelection(firstSelect);
      },
      brokeredClear: function () {
        view.selection.clear();
        view.updateSelection();
80 81
      },
    };
82
    view.selection = new MySelection(sourcePositionToStringKey);
83 84 85 86
    broker.addSourcePositionHandler(selectionHandler);
    this.selectionHandler = selectionHandler;
    this.initializeCode();
  }
87

88 89 90 91
  addHtmlElementToSourcePosition(sourcePosition, element) {
    const key = sourcePositionToStringKey(sourcePosition);
    if (this.sourcePositionToHtmlElement.has(key)) {
      console.log("Warning: duplicate source position", sourcePosition);
92
    }
93 94
    this.sourcePositionToHtmlElement.set(key, element);
  }
95

96
  getHtmlElementForSourcePosition(sourcePosition) {
97
    const key = sourcePositionToStringKey(sourcePosition);
98 99 100
    return this.sourcePositionToHtmlElement.get(key);
  }

101
  updateSelection(scrollIntoView: boolean = false): void {
102
    const mkVisible = new ViewElements(this.divNode.parentNode as HTMLElement);
103 104 105 106 107 108 109
    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);
  }
110

111 112
  getCodeHtmlElementName() {
    return `source-pre-${this.source.sourceId}`;
113
  }
114

115 116 117
  getCodeHeaderHtmlElementName() {
    return `source-pre-${this.source.sourceId}-header`;
  }
118

119 120 121
  getHtmlCodeLines(): NodeListOf<HTMLElement> {
    const ordereList = this.divNode.querySelector(`#${this.getCodeHtmlElementName()} ol`);
    return ordereList.childNodes as NodeListOf<HTMLElement>;
122 123
  }

124
  onSelectLine(lineNumber: number, doClear: boolean) {
125 126 127
    if (doClear) {
      this.selectionHandler.clear();
    }
128
    const positions = this.sourceResolver.linetoSourcePositions(lineNumber - 1);
129 130 131
    if (positions !== undefined) {
      this.selectionHandler.select(positions, undefined);
    }
132 133
  }

134
  onSelectSourcePosition(sourcePosition, doClear: boolean) {
135 136 137 138 139 140 141
    if (doClear) {
      this.selectionHandler.clear();
    }
    this.selectionHandler.select([sourcePosition], undefined);
  }

  initializeCode() {
142
    const view = this;
143 144 145 146
    const source = this.source;
    const sourceText = source.sourceText;
    if (!sourceText) return;
    const sourceContainer = view.divNode;
147
    if (this.codeMode == CodeMode.MAIN_SOURCE) {
148 149 150 151
      sourceContainer.classList.add("main-source");
    } else {
      sourceContainer.classList.add("inlined-source");
    }
152
    const codeHeader = document.createElement("div");
153 154
    codeHeader.setAttribute("id", this.getCodeHeaderHtmlElementName());
    codeHeader.classList.add("code-header");
155
    const codeFileFunction = document.createElement("div");
156 157 158
    codeFileFunction.classList.add("code-file-function");
    codeFileFunction.innerHTML = `${source.sourceName}:${source.functionName}`;
    codeHeader.appendChild(codeFileFunction);
159
    const codeModeDiv = document.createElement("div");
160 161 162
    codeModeDiv.classList.add("code-mode");
    codeModeDiv.innerHTML = `${this.codeMode}`;
    codeHeader.appendChild(codeModeDiv);
163 164
    const clearDiv = document.createElement("div");
    clearDiv.style.clear = "both";
165 166
    codeHeader.appendChild(clearDiv);
    sourceContainer.appendChild(codeHeader);
167
    const codePre = document.createElement("pre");
168
    codePre.setAttribute("id", this.getCodeHtmlElementName());
169
    codePre.classList.add("prettyprint");
170 171 172 173 174 175 176 177
    sourceContainer.appendChild(codePre);

    codeHeader.onclick = function myFunction() {
      if (codePre.style.display === "none") {
        codePre.style.display = "block";
      } else {
        codePre.style.display = "none";
      }
178
    };
179 180 181
    if (sourceText != "") {
      codePre.classList.add("linenums");
      codePre.textContent = sourceText;
182 183
      try {
        // Wrap in try to work when offline.
184
        PR.prettyPrint(undefined, sourceContainer);
185
      } catch (e) {
186
        console.log(e);
187
      }
188

189 190 191 192 193 194 195 196 197 198
      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();
        }
199
      };
200

201
      const base: number = source.startPosition;
202 203 204
      let current = 0;
      const lineListDiv = this.getHtmlCodeLines();
      let newlineAdjust = 0;
205
      for (let i = 0; i < lineListDiv.length; i++) {
206 207 208
        // Line numbers are not zero-based.
        const lineNumber = i + 1;
        const currentLineElement = lineListDiv[i];
209
        currentLineElement.id = "li" + i;
210
        currentLineElement.dataset.lineNumber = "" + lineNumber;
211
        const spans = currentLineElement.childNodes;
212
        for (const currentSpan of spans) {
213 214 215 216 217 218 219
          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;
          }
220
        }
221 222 223

        this.insertLineNumber(currentLineElement, lineNumber);

224
        while ((current < sourceText.length) &&
225
          (sourceText[current] == '\n' || sourceText[current] == '\r')) {
226
          ++current;
227
          ++newlineAdjust;
228 229 230
        }
      }
    }
231
  }
232

233 234 235
  insertSourcePositions(currentSpan, lineNumber, pos, end, adjust) {
    const view = this;
    const sps = this.sourceResolver.sourcePositionsInRange(this.source.sourceId, pos - adjust, end);
236
    let offset = 0;
237
    for (const sourcePosition of sps) {
238
      this.sourceResolver.addAnyPositionToLine(lineNumber, sourcePosition);
239
      const textnode = currentSpan.tagName == 'SPAN' ? currentSpan.lastChild : currentSpan;
240
      if (!(textnode instanceof Text)) continue;
241 242 243
      const splitLength = Math.max(0, sourcePosition.scriptOffset - pos - offset);
      offset += splitLength;
      const replacementNode = textnode.splitText(splitLength);
244 245
      const span = document.createElement('span');
      span.setAttribute("scriptOffset", sourcePosition.scriptOffset);
246
      span.classList.add("source-position");
247
      const marker = document.createElement('span');
248
      marker.classList.add("marker");
249 250 251 252 253
      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');
254 255
        inliningMarker.classList.add("inlining-marker");
        inliningMarker.setAttribute("data-descr", `${sourceName} was inlined here`);
256 257 258 259
        span.appendChild(inliningMarker);
      }
      span.onclick = function (e) {
        e.stopPropagation();
260
        view.onSelectSourcePosition(sourcePosition, !e.shiftKey);
261 262 263 264 265 266
      };
      view.addHtmlElementToSourcePosition(sourcePosition, span);
      textnode.parentNode.insertBefore(span, replacementNode);
    }
  }

267
  insertLineNumber(lineElement: HTMLElement, lineNumber: number) {
268 269 270
    const view = this;
    const lineNumberElement = document.createElement("div");
    lineNumberElement.classList.add("line-number");
271 272
    lineNumberElement.dataset.lineNumber = `${lineNumber}`;
    lineNumberElement.innerText = `${lineNumber}`;
273
    lineElement.insertBefore(lineNumberElement, lineElement.firstChild);
274
    // Don't add lines to source positions of not in backwardsCompatibility mode.
275 276 277 278
    if (this.source.backwardsCompatibility === true) {
      for (const sourcePosition of this.sourceResolver.linetoSourcePositions(lineNumber - 1)) {
        view.addHtmlElementToSourcePosition(sourcePosition, lineElement);
      }
279
    }
280 281
  }

282
}