source.js (10713B)
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 /** 6 * Utils for working with Source URLs 7 * 8 * @module utils/source 9 */ 10 11 const { 12 getUnicodeUrl, 13 } = require("resource://devtools/client/shared/unicode-url.js"); 14 const { 15 micromatch, 16 } = require("resource://devtools/client/shared/vendor/micromatch/micromatch.js"); 17 18 import { getRelativePath } from "../utils/sources-tree/utils"; 19 import { endTruncateStr } from "./utils"; 20 import { truncateMiddleText } from "../utils/text"; 21 import { memoizeLast } from "../utils/memoizeLast"; 22 import { toWasmSourceLine, getEditor } from "./editor/index"; 23 export { isMinified } from "./isMinified"; 24 25 import { isFulfilled } from "./async-value"; 26 27 export const sourceTypes = { 28 coffee: "coffeescript", 29 js: "javascript", 30 jsx: "react", 31 ts: "typescript", 32 tsx: "typescript", 33 vue: "vue", 34 }; 35 36 export const javascriptLikeExtensions = new Set(["marko", "es6", "vue", "jsm"]); 37 38 function getPath(source) { 39 const { path } = source.displayURL; 40 let lastIndex = path.lastIndexOf("/"); 41 let nextToLastIndex = path.lastIndexOf("/", lastIndex - 1); 42 43 const result = []; 44 do { 45 result.push(path.slice(nextToLastIndex + 1, lastIndex)); 46 lastIndex = nextToLastIndex; 47 nextToLastIndex = path.lastIndexOf("/", lastIndex - 1); 48 } while (lastIndex !== nextToLastIndex); 49 50 result.push(""); 51 52 return result; 53 } 54 55 export function shouldBlackbox(source) { 56 if (!source) { 57 return false; 58 } 59 60 if (!source.url) { 61 return false; 62 } 63 64 return true; 65 } 66 67 /** 68 * Checks if the frame is within a line ranges which are blackboxed 69 * in the source. 70 * 71 * @param {object} frame 72 * The current frame 73 * @param {object} blackboxedRanges 74 * The currently blackboxedRanges for all the sources. 75 * @param {boolean} isFrameBlackBoxed 76 * If the frame is within the blackboxed range 77 * or not. 78 */ 79 export function isFrameBlackBoxed(frame, blackboxedRanges) { 80 const { source } = frame.location; 81 return ( 82 !!blackboxedRanges[source.url] && 83 (!blackboxedRanges[source.url].length || 84 !!findBlackBoxRange(source, blackboxedRanges, { 85 start: frame.location.line, 86 end: frame.location.line, 87 })) 88 ); 89 } 90 91 /** 92 * Checks if a blackbox range exist for the line range. 93 * That is if any start and end lines overlap any of the 94 * blackbox ranges 95 * 96 * @param {object} source 97 * The current selected source 98 * @param {object} blackboxedRanges 99 * The store of blackboxedRanges 100 * @param {object} lineRange 101 * The start/end line range `{ start: <Number>, end: <Number> }` 102 * @return {object} blackboxRange 103 * The first matching blackbox range that all or part of the 104 * specified lineRange sits within. 105 */ 106 export function findBlackBoxRange(source, blackboxedRanges, lineRange) { 107 const ranges = blackboxedRanges[source.url]; 108 if (!ranges || !ranges.length) { 109 return null; 110 } 111 112 return ranges.find( 113 range => 114 (lineRange.start >= range.start.line && 115 lineRange.start <= range.end.line) || 116 (lineRange.end >= range.start.line && lineRange.end <= range.end.line) 117 ); 118 } 119 120 /** 121 * Checks if a source line is blackboxed 122 * 123 * @param {Array} ranges - Line ranges that are blackboxed 124 * @param {number} line 125 * @param {boolean} isSourceOnIgnoreList - is the line in a source that is on 126 * the sourcemap ignore lists then the line is blackboxed. 127 * @returns boolean 128 */ 129 export function isLineBlackboxed(ranges, line, isSourceOnIgnoreList) { 130 if (isSourceOnIgnoreList) { 131 return true; 132 } 133 134 if (!ranges) { 135 return false; 136 } 137 // If the whole source is ignored , then the line is 138 // ignored. 139 if (!ranges.length) { 140 return true; 141 } 142 return !!ranges.find( 143 range => line >= range.start.line && line <= range.end.line 144 ); 145 } 146 147 /** 148 * Returns true if the specified url and/or content type are specific to 149 * javascript files. 150 * 151 * @return boolean 152 * True if the source is likely javascript. 153 * 154 * @memberof utils/source 155 * @static 156 */ 157 export function isJavaScript(source, content) { 158 const extension = source.displayURL.fileExtension; 159 const contentType = content.type === "wasm" ? null : content.contentType; 160 return ( 161 javascriptLikeExtensions.has(extension) || 162 !!(contentType && contentType.includes("javascript")) 163 ); 164 } 165 166 export function isPrettyURL(url) { 167 return url ? url.endsWith(":formatted") : false; 168 } 169 170 /** 171 * @memberof utils/source 172 * @static 173 */ 174 export function getPrettySourceURL(url) { 175 if (!url) { 176 url = ""; 177 } 178 return `${url}:formatted`; 179 } 180 181 /** 182 * @memberof utils/source 183 * @static 184 */ 185 export function getRawSourceURL(url) { 186 return url && url.endsWith(":formatted") 187 ? url.slice(0, -":formatted".length) 188 : url; 189 } 190 191 function resolveFileURL( 192 url, 193 transformUrl = initialUrl => initialUrl, 194 truncate = true 195 ) { 196 url = getRawSourceURL(url || ""); 197 const name = transformUrl(url); 198 if (!truncate) { 199 return name; 200 } 201 return endTruncateStr(name, 50); 202 } 203 204 export function getFormattedSourceId(id) { 205 if (typeof id != "string") { 206 console.error( 207 "Expected source id to be a string, got", 208 typeof id, 209 " | id:", 210 id 211 ); 212 return ""; 213 } 214 return id.substring(id.lastIndexOf("/") + 1); 215 } 216 217 /** 218 * Provides a middle-truncated filename displayed in Tab titles 219 */ 220 export function getTruncatedFileName(source) { 221 return truncateMiddleText(source.longName, 30); 222 } 223 224 /** 225 * Gets path for files with same filename for editor tabs, breakpoints, etc. 226 * Pass the source, and list of other sources 227 * 228 * @memberof utils/source 229 * @static 230 */ 231 232 export function getDisplayPath(mySource, sources) { 233 const rawSourceURL = getRawSourceURL(mySource.url); 234 const filename = mySource.shortName; 235 236 // Find sources that have the same filename, but different paths 237 // as the original source 238 const similarSources = sources.filter(source => { 239 const rawSource = getRawSourceURL(source.url); 240 return rawSourceURL != rawSource && filename == source.shortName; 241 }); 242 243 if (!similarSources.length) { 244 return undefined; 245 } 246 247 // get an array of source path directories e.g. ['a/b/c.html'] => [['b', 'a']] 248 const paths = new Array(similarSources.length + 1); 249 250 paths[0] = getPath(mySource); 251 for (let i = 0; i < similarSources.length; ++i) { 252 paths[i + 1] = getPath(similarSources[i]); 253 } 254 255 // create an array of similar path directories and one dis-similar directory 256 // for example [`a/b/c.html`, `a1/b/c.html`] => ['b', 'a'] 257 // where 'b' is the similar directory and 'a' is the dis-similar directory. 258 let displayPath = ""; 259 for (let i = 0; i < paths[0].length; i++) { 260 let similar = false; 261 for (let k = 1; k < paths.length; ++k) { 262 if (paths[k][i] === paths[0][i]) { 263 similar = true; 264 break; 265 } 266 } 267 268 displayPath = paths[0][i] + (i !== 0 ? "/" : "") + displayPath; 269 270 if (!similar) { 271 break; 272 } 273 } 274 275 return displayPath; 276 } 277 278 /** 279 * Gets a readable source URL for display purposes. 280 * If the source does not have a URL, the source ID will be returned instead. 281 * 282 * @memberof utils/source 283 * @static 284 */ 285 export function getFileURL(source, truncate = true) { 286 const { url, id } = source; 287 if (!url) { 288 return getFormattedSourceId(id); 289 } 290 291 return resolveFileURL(url, getUnicodeUrl, truncate); 292 } 293 294 function getNthLine(str, lineNum) { 295 let startIndex = -1; 296 297 let newLinesFound = 0; 298 while (newLinesFound < lineNum) { 299 const nextIndex = str.indexOf("\n", startIndex + 1); 300 if (nextIndex === -1) { 301 return null; 302 } 303 startIndex = nextIndex; 304 newLinesFound++; 305 } 306 const endIndex = str.indexOf("\n", startIndex + 1); 307 if (endIndex === -1) { 308 return str.slice(startIndex + 1); 309 } 310 311 return str.slice(startIndex + 1, endIndex); 312 } 313 314 export const getLineText = memoizeLast((sourceId, asyncContent, line) => { 315 if (!asyncContent || !isFulfilled(asyncContent)) { 316 return ""; 317 } 318 319 const content = asyncContent.value; 320 321 if (content.type === "wasm") { 322 const editor = getEditor(); 323 const lines = editor.renderWasmText(content); 324 return lines[toWasmSourceLine(line)] || ""; 325 } 326 327 const lineText = getNthLine(content.value, line - 1); 328 return lineText || ""; 329 }); 330 331 export function getTextAtPosition(sourceId, asyncContent, location) { 332 const { column, line = 0 } = location; 333 334 const lineText = getLineText(sourceId, asyncContent, line); 335 return lineText.slice(column, column + 100).trim(); 336 } 337 338 /** 339 * Compute the CSS classname string to use for the icon of a given source. 340 * 341 * @param {object} source 342 * The reducer source object. 343 * @param {boolean} isBlackBoxed 344 * To be set to true, when the given source is blackboxed. 345 * but another tab for that source is opened pretty printed. 346 * @return String 347 * The classname to use. 348 */ 349 export function getSourceClassnames(source, isBlackBoxed) { 350 // Conditionals should be ordered by priority of icon! 351 const defaultClassName = "file"; 352 353 if (!source || !source.url) { 354 return defaultClassName; 355 } 356 357 if (isBlackBoxed) { 358 return "blackBox"; 359 } 360 361 if (isUrlExtension(source.url)) { 362 return "extension"; 363 } 364 365 return sourceTypes[source.displayURL.fileExtension] || defaultClassName; 366 } 367 368 export function getRelativeUrl(source, root) { 369 const { group, path } = source.displayURL; 370 if (!root) { 371 return path; 372 } 373 374 // + 1 removes the leading "/" 375 const url = group + path; 376 return url.slice(url.indexOf(root) + root.length + 1); 377 } 378 379 export function isUrlExtension(url) { 380 return url.includes("moz-extension:") || url.includes("chrome-extension"); 381 } 382 383 /** 384 * Checks that source url matches one of the glob patterns 385 * 386 * @param {object} source 387 * @param {string} excludePatterns 388 String of comma-seperated glob patterns 389 * @return {return} Boolean value specifies if the string matches any 390 of the patterns. 391 */ 392 export function matchesGlobPatterns(source, excludePatterns) { 393 if (!excludePatterns) { 394 return false; 395 } 396 const patterns = excludePatterns 397 .split(",") 398 .map(pattern => pattern.trim()) 399 .filter(pattern => pattern !== ""); 400 401 if (!patterns.length) { 402 return false; 403 } 404 405 return micromatch.contains( 406 // Makes sure we format the url or id exactly the way its displayed in the search ui, 407 // as user wil usually create glob patterns based on what is seen in the ui. 408 source.url ? getRelativePath(source.url) : getFormattedSourceId(source.id), 409 patterns 410 ); 411 }