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 }