tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

tokens.js (5340B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
      4 
      5 function _isInvalidTarget(target) {
      6  if (!target || !target.innerText) {
      7    return true;
      8  }
      9 
     10  const tokenText = target.innerText.trim();
     11 
     12  // exclude syntax where the expression would be a syntax error
     13  const invalidToken =
     14    tokenText === "" || tokenText.match(/^[(){}\|&%,.;=<>\+-/\*\s](?=)/);
     15  if (invalidToken) {
     16    return true;
     17  }
     18 
     19  // exclude tokens for which it does not make sense to show a preview:
     20  // - literal
     21  // - primitives
     22  // - operators
     23  // - tags
     24  const INVALID_TARGET_CLASSES = [
     25    // CM6 tokens,
     26    "tok-string",
     27    "tok-punctuation",
     28    "tok-number",
     29    "tok-bool",
     30    "tok-operator",
     31    // also exclude editor element (defined in Editor component)
     32    "editor-mount",
     33  ];
     34  if (
     35    target.className === "" ||
     36    INVALID_TARGET_CLASSES.some(cls => target.classList.contains(cls))
     37  ) {
     38    return true;
     39  }
     40 
     41  // `undefined` isn't flagged with any useful class name to ignore it
     42  if (
     43    target.classList.contains("tok-variableName") &&
     44    tokenText == "undefined"
     45  ) {
     46    return true;
     47  }
     48 
     49  // We need to exclude keywords, but since codeMirror tags "this" as a keyword, we need
     50  // to check the tokenText as well.
     51  // This seems to be the only case that we want to exclude (see devtools/client/shared/sourceeditor/codemirror/mode/javascript/javascript.js#24-41)
     52  // For CM6 https://github.com/codemirror/lang-javascript/blob/7edd3df9b0df41aef7c9835efac53fb52b747282/src/javascript.ts#L79
     53  if (
     54    (target.classList.contains("cm-keyword") ||
     55      target.classList.contains("tok-keyword")) &&
     56    tokenText !== "this"
     57  ) {
     58    return true;
     59  }
     60 
     61  // exclude codemirror elements that are not tokens
     62  if (
     63    // exclude inline preview
     64    target.closest(".CodeMirror-widget") ||
     65    target.closest(".inline-preview") ||
     66    // exclude in-line "empty" space, as well as the gutter
     67    target.matches(".CodeMirror-line, .CodeMirror-gutter-elt") ||
     68    // exclude items that are not in a line
     69    (!target.closest(".CodeMirror-line") &&
     70      // exclude items that are not in a line  for CM6
     71      !target.closest(".cm-line")) ||
     72    target.getBoundingClientRect().top == 0 ||
     73    // exclude selecting the whole line, CM6
     74    target.classList.contains("cm-line")
     75  ) {
     76    return true;
     77  }
     78 
     79  // exclude popup
     80  if (target.closest(".popover")) {
     81    return true;
     82  }
     83 
     84  return false;
     85 }
     86 
     87 function _dispatch(editor, eventName, data) {
     88  editor.emit(eventName, data);
     89 }
     90 
     91 function _invalidLeaveTarget(target) {
     92  if (!target || target.closest(".popover")) {
     93    return true;
     94  }
     95 
     96  return false;
     97 }
     98 
     99 /**
    100 * Wraps the codemirror mouse events  to generate token events
    101 *
    102 * @param {object} editor
    103 * @returns {Function}
    104 */
    105 export function onMouseOver(editor) {
    106  let prevTokenPos = null;
    107 
    108  function onMouseLeave(event) {
    109    // mouseleave's `relatedTarget` is the DOM element we entered to.
    110    // If we enter into any element within the popup, ignore the mouseleave
    111    // and track the leave from that new hovered element.
    112    //
    113    // This typicaly happens when moving from the token to the popup,
    114    // but also from popup to the popup "gap",
    115    if (_invalidLeaveTarget(event.relatedTarget)) {
    116      addMouseLeave(event.relatedTarget);
    117      return;
    118    }
    119 
    120    prevTokenPos = null;
    121    _dispatch(editor, "tokenleave", event);
    122  }
    123 
    124  function addMouseLeave(target) {
    125    target.addEventListener("mouseleave", onMouseLeave, {
    126      capture: true,
    127      once: true,
    128    });
    129  }
    130 
    131  return enterEvent => {
    132    const { target } = enterEvent;
    133 
    134    if (_isInvalidTarget(target)) {
    135      return;
    136    }
    137    const tokenPos = getTokenLocation(editor, target);
    138 
    139    if (
    140      prevTokenPos?.line !== tokenPos?.line ||
    141      prevTokenPos?.column !== tokenPos?.column
    142    ) {
    143      addMouseLeave(target);
    144 
    145      _dispatch(editor, "tokenenter", {
    146        event: enterEvent,
    147        target,
    148        tokenPos,
    149      });
    150      prevTokenPos = tokenPos;
    151    }
    152  };
    153 }
    154 
    155 /**
    156 * Gets the end position of a token at a specific line/column
    157 *
    158 * @param {*} codeMirror
    159 * @param {number} line
    160 * @param {number} column
    161 * @returns {number}
    162 */
    163 export function getTokenEnd(codeMirror, line, column) {
    164  const token = codeMirror.getTokenAt({
    165    line,
    166    ch: column + 1,
    167  });
    168  const tokenString = token.string;
    169 
    170  return tokenString === "{" || tokenString === "[" ? null : token.end;
    171 }
    172 
    173 /**
    174 * Given the dom element related to the token, this gets its line and column.
    175 *
    176 * @param {*} editor
    177 * @param {*} tokenEl
    178 * @returns {object} An object of the form { line, column }
    179 */
    180 export function getTokenLocation(editor, tokenEl) {
    181  // Get the quad (and not the bounding rect), as the span could wrap on multiple lines
    182  // and the middle of the bounding rect may not be over the token:
    183  // +───────────────────────+
    184  // │      myLongVariableNa│
    185  // │me         +          │
    186  // +───────────────────────+
    187  const { p1, p2, p3 } = tokenEl.getBoxQuads()[0];
    188  const left = p1.x + (p2.x - p1.x) / 2;
    189  const top = p1.y + (p3.y - p1.y) / 2;
    190  return editor.getPositionAtScreenCoords(left, top);
    191 }