select.js (17564B)
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 * Redux actions for the sources state 7 * 8 * @module actions/sources 9 */ 10 import { prettyPrintSource } from "./prettyPrint"; 11 import { addTab } from "../tabs"; 12 import { loadSourceText } from "./loadSourceText"; 13 import { setBreakableLines } from "./breakableLines"; 14 import { prefs } from "../../utils/prefs"; 15 16 import { createLocation } from "../../utils/location"; 17 import { 18 getRelatedMapLocation, 19 getOriginalLocation, 20 getGeneratedLocation, 21 } from "../../utils/source-maps"; 22 23 import { 24 getSource, 25 getFirstSourceActorForGeneratedSource, 26 getSourceByURL, 27 getSelectedLocation, 28 getShouldSelectOriginalLocation, 29 tabExists, 30 hasSource, 31 hasSourceActor, 32 isPrettyPrinted, 33 isPrettyPrintedDisabled, 34 isSourceActorWithSourceMap, 35 getSelectedTraceIndex, 36 } from "../../selectors/index"; 37 38 // This is only used by jest tests (and within this module) 39 export const setSelectedLocation = ( 40 location, 41 shouldSelectOriginalLocation, 42 shouldHighlightSelectedLocation, 43 shouldScrollToSelectedLocation 44 ) => ({ 45 type: "SET_SELECTED_LOCATION", 46 location, 47 shouldSelectOriginalLocation, 48 shouldHighlightSelectedLocation, 49 shouldScrollToSelectedLocation, 50 }); 51 52 // This is only used by jest tests (and within this module) 53 export const setPendingSelectedLocation = (url, options) => ({ 54 type: "SET_PENDING_SELECTED_LOCATION", 55 url, 56 line: options?.line, 57 column: options?.column, 58 }); 59 60 // This is only used by jest tests (and within this module) 61 export const clearSelectedLocation = () => ({ 62 type: "CLEAR_SELECTED_LOCATION", 63 }); 64 65 export const setDefaultSelectedLocation = shouldSelectOriginalLocation => ({ 66 type: "SET_DEFAULT_SELECTED_LOCATION", 67 shouldSelectOriginalLocation, 68 }); 69 70 /** 71 * Deterministically select a source that has a given URL. This will 72 * work regardless of the connection status or if the source exists 73 * yet. 74 * 75 * This exists mostly for external things to interact with the 76 * debugger. 77 */ 78 export function selectSourceURL(url, options) { 79 return async ({ dispatch, getState }) => { 80 const source = getSourceByURL(getState(), url); 81 if (!source) { 82 return dispatch(setPendingSelectedLocation(url, options)); 83 } 84 85 const location = createLocation({ ...options, source }); 86 return dispatch(selectLocation(location)); 87 }; 88 } 89 90 /** 91 * Wrapper around selectLocation, which creates the location object for us. 92 * Note that it ignores the currently selected source and will select 93 * the precise generated/original source passed as argument. 94 * 95 * @param {string} source 96 * The precise source to select. 97 * @param {string} sourceActor 98 * The specific source actor of the source to 99 * select the source text. This is optional. 100 */ 101 export function selectSource(source, sourceActor) { 102 return async ({ dispatch }) => { 103 // `createLocation` requires a source object, but we may use selectSource to close the last tab, 104 // where source will be null and the location will be an empty object. 105 const location = source ? createLocation({ source, sourceActor }) : {}; 106 107 return dispatch(selectSpecificLocation(location)); 108 }; 109 } 110 111 /** 112 * Helper for `selectLocation`. 113 * Based on `keepContext` argument passed to `selectLocation`, 114 * this will automatically select the related mapped source (original or generated). 115 * 116 * @param {object} location 117 * The location to select. 118 * @param {boolean} keepContext 119 * If true, will try to select a mapped source. 120 * @param {object} thunkArgs 121 * @return {object} 122 * Object with two attributes: 123 * - `shouldSelectOriginalLocation`, to know if we should keep trying to select the original location 124 * - `newLocation`, for the final location to select 125 */ 126 async function mayBeSelectMappedSource(location, keepContext, thunkArgs) { 127 const { getState, dispatch } = thunkArgs; 128 129 // Preserve the current source map context (original / generated) 130 // when navigating to a new location. 131 // i.e. if keepContext isn't manually overriden to false, 132 // we will convert the source we want to select to either 133 // original/generated in order to match the currently selected one. 134 // If the currently selected source is original, we will 135 // automatically map `location` to refer to the original source, 136 // even if that used to refer only to the generated source. 137 let shouldSelectOriginalLocation = 138 getShouldSelectOriginalLocation(getState()); 139 140 // Pretty print source may not be registered yet and getRelatedMapLocation may not return it. 141 // Wait for the pretty print source to be fully processed. 142 // 143 // In this case we don't follow the "should select original location", 144 // we solely follow user decision to have pretty printed the source. 145 const sourceIsPrettyPrinted = isPrettyPrinted(getState(), location.source); 146 const shouldPrettyPrint = 147 !location.source.isOriginal && 148 (sourceIsPrettyPrinted || 149 (prefs.autoPrettyPrint && 150 !isPrettyPrintedDisabled(getState(), location.source))); 151 152 if (shouldPrettyPrint) { 153 const isAutoPrettyPrinting = 154 !sourceIsPrettyPrinted && prefs.autoPrettyPrint; 155 // Note that prettyPrintSource has already been called a bit before when this generated source has been added 156 // but it is a slow operation and is most likely not resolved yet. 157 // `prettyPrintSource` uses memoization to avoid doing the operation more than once, while waiting from both callsites. 158 const prettyPrintedSource = await dispatch( 159 prettyPrintSource({ source: location.source, isAutoPrettyPrinting }) 160 ); 161 162 // Return to the current location if the source can't be pretty printed 163 if (!prettyPrintedSource) { 164 return { shouldSelectOriginalLocation, newLocation: location }; 165 } 166 167 // If we aren't selecting a particular location line will be 0 and column be undefined, 168 // avoid calling getRelatedMapLocation which may not map to any original location. 169 if (location.line == 0 && !location.column) { 170 return { 171 shouldSelectOriginalLocation, 172 newLocation: createLocation({ 173 ...location, 174 source: prettyPrintedSource, 175 line: 1, 176 column: 0, 177 }), 178 }; 179 } 180 location = await getRelatedMapLocation(location, thunkArgs); 181 return { shouldSelectOriginalLocation, newLocation: location }; 182 } 183 184 if (keepContext) { 185 if (shouldSelectOriginalLocation != location.source.isOriginal) { 186 // Only try to map the location if the source is mapped: 187 // - mapping from original to generated, if this is original source 188 // - mapping from generated to original, if the generated source has a source map URL comment 189 // - mapping from compressed to pretty print, if the compressed source has a matching pretty print tab opened 190 if ( 191 location.source.isOriginal || 192 isSourceActorWithSourceMap(getState(), location.sourceActor.id) || 193 sourceIsPrettyPrinted 194 ) { 195 // getRelatedMapLocation will convert to the related generated/original location. 196 // i.e if the original location is passed, the related generated location will be returned and vice versa. 197 location = await getRelatedMapLocation(location, thunkArgs); 198 } 199 // Note that getRelatedMapLocation may return the exact same location. 200 // For example, if the source-map is half broken, it may return a generated location 201 // while we were selecting original locations. So we may be seeing bundles intermittently 202 // when stepping through broken source maps. And we will see original sources when stepping 203 // through functional original sources. 204 } 205 } else if ( 206 location.source.isOriginal || 207 isSourceActorWithSourceMap(getState(), location.sourceActor.id) 208 ) { 209 // Only update this setting if the source is mapped. i.e. don't update if we select a regular source. 210 // The source is mapped when it is either: 211 // - an original source, 212 // - a bundle with a source map comment referencing a source map URL. 213 shouldSelectOriginalLocation = location.source.isOriginal; 214 } 215 return { shouldSelectOriginalLocation, newLocation: location }; 216 } 217 218 /** 219 * Select a new location. 220 * This will automatically select the source in the source tree (if visible) 221 * and open the source (a new tab and the source editor) 222 * as well as highlight a precise line in the editor. 223 * 224 * Note that by default, this may map your passed location to the original 225 * or generated location based on the selected source state. (see keepContext) 226 * 227 * @param {object} location 228 * @param {object} options 229 * @param {boolean} options.keepContext 230 * If false, this will ignore the currently selected source 231 * and select the generated or original location, even if we 232 * were currently selecting the other source type. 233 * @param {boolean} options.highlight 234 * True by default. To be set to false in order to preveng highlighting the selected location in the editor. 235 * We will only show the location, but do not put a special background on the line. 236 * @param {boolean} options.scroll 237 * True by default. Is set to false to stop the editor from scrolling to the location that has been selected. 238 * e.g is when clicking in the editor to just show the selected line / column in the footer 239 */ 240 export function selectLocation( 241 location, 242 { keepContext = true, highlight = true, scroll = true } = {} 243 ) { 244 // eslint-disable-next-line complexity 245 return async thunkArgs => { 246 const { dispatch, getState, client } = thunkArgs; 247 248 if (!client) { 249 // No connection, do nothing. This happens when the debugger is 250 // shut down too fast and it tries to display a default source. 251 return; 252 } 253 254 let source = location.source; 255 256 if (!source) { 257 // If there is no source we deselect the current selected source 258 dispatch(clearSelectedLocation()); 259 return; 260 } 261 262 const lastSelectedTraceIndex = getSelectedTraceIndex(getState()); 263 264 let sourceActor = location.sourceActor; 265 if (!sourceActor) { 266 sourceActor = getFirstSourceActorForGeneratedSource( 267 getState(), 268 source.id 269 ); 270 location = createLocation({ ...location, sourceActor }); 271 } 272 273 const lastSelectedLocation = getSelectedLocation(getState()); 274 const { shouldSelectOriginalLocation, newLocation } = 275 await mayBeSelectMappedSource(location, keepContext, thunkArgs); 276 277 // Ignore the request if another location was selected while we were waiting for mayBeSelectMappedSource async completion 278 if (getSelectedLocation(getState()) != lastSelectedLocation) { 279 return; 280 } 281 282 // Update all local variables after mapping 283 location = newLocation; 284 source = location.source; 285 sourceActor = location.sourceActor; 286 if (!sourceActor) { 287 sourceActor = getFirstSourceActorForGeneratedSource( 288 getState(), 289 source.id 290 ); 291 location = createLocation({ ...location, sourceActor }); 292 } 293 294 if (!tabExists(getState(), source)) { 295 dispatch(addTab(source)); 296 } 297 dispatch( 298 setSelectedLocation( 299 location, 300 shouldSelectOriginalLocation, 301 highlight, 302 scroll 303 ) 304 ); 305 306 await dispatch(loadSourceText(source, sourceActor)); 307 308 // Stop the async work if we started selecting another location 309 if (getSelectedLocation(getState()) != location) { 310 return; 311 } 312 313 await dispatch(setBreakableLines(location)); 314 315 // Stop the async work if we started selecting another location 316 if (getSelectedLocation(getState()) != location) { 317 return; 318 } 319 320 const loadedSource = getSource(getState(), source.id); 321 322 if (!loadedSource) { 323 // If there was a navigation while we were loading the loadedSource 324 return; 325 } 326 327 // When we select a generated source which has a sourcemap, 328 // asynchronously fetch the related original location in order to display 329 // the mapped location in the editor's footer. 330 if ( 331 !location.source.isOriginal && 332 isSourceActorWithSourceMap(getState(), sourceActor.id) 333 ) { 334 let originalLocation = await getOriginalLocation(location, thunkArgs, { 335 looseSearch: true, 336 }); 337 // We pass a null original location when the location doesn't map 338 // in order to know when we are done processing the source map. 339 // * `getOriginalLocation` would return the exact same location if it doesn't map 340 // * `getOriginalLocation` may also return a distinct location object, 341 // but refering to the same `source` object (which is the bundle) when it doesn't 342 // map to any known original location. 343 if (originalLocation.source === location.source) { 344 originalLocation = null; 345 } 346 dispatch({ 347 type: "SET_ORIGINAL_SELECTED_LOCATION", 348 location, 349 originalLocation, 350 }); 351 } 352 353 // Also store the mapped generated location for the tracer which uses generated locations only. 354 if (location.source.isOriginal) { 355 const generatedLocation = await getGeneratedLocation(location, thunkArgs); 356 // We may concurrently race mutiples calls to selectTrace action, which is going to call selectLocation 357 // We should ignore and bail out if the selected trace changed while resolving the generated location. 358 if (getSelectedTraceIndex(getState()) != lastSelectedTraceIndex) { 359 return; 360 } 361 362 // Bail out if the selection changed to another one while getGeneratedLocation was computing. 363 if (getSelectedLocation(getState()) != location) { 364 return; 365 } 366 367 if (!generatedLocation.sourceActor) { 368 generatedLocation.sourceActor = getFirstSourceActorForGeneratedSource( 369 getState(), 370 generatedLocation.source.id 371 ); 372 } 373 374 dispatch({ 375 type: "SET_GENERATED_SELECTED_LOCATION", 376 location, 377 generatedLocation, 378 }); 379 } 380 }; 381 } 382 383 /** 384 * Select a location while ignoring the currently selected source. 385 * This will select the generated location even if the currently 386 * select source is an original source. And the other way around. 387 * 388 * @param {object} location 389 * The location to select, object which includes enough 390 * information to specify a precise source, line and column. 391 */ 392 export function selectSpecificLocation(location) { 393 return selectLocation(location, { keepContext: false }); 394 } 395 396 /** 397 * Similar to `selectSpecificLocation`, but if the precise Source object 398 * is missing, this will fallback to select any source having the same URL. 399 * In this fallback scenario, sources without a URL will be ignored. 400 * 401 * This is typically used when trying to select a source (e.g. in project search result) 402 * after reload, because the source objects are new on each new page load, but source 403 * with the same URL may still exist. 404 * 405 * @param {object} location 406 * The location to select. 407 * @return {function} 408 * The action will return true if a matching source was found. 409 */ 410 export function selectSpecificLocationOrSameUrl(location) { 411 return async ({ dispatch, getState }) => { 412 // If this particular source no longer exists, open any matching URL. 413 // This will typically happen on reload. 414 if (!hasSource(getState(), location.source.id)) { 415 // Some sources, like evaled script won't have a URL attribute 416 // and can't be re-selected if we don't find the exact same source object. 417 if (!location.source.url) { 418 return false; 419 } 420 const source = getSourceByURL(getState(), location.source.url); 421 if (!source) { 422 return false; 423 } 424 // Also reset the sourceActor, as it won't match the same source. 425 const sourceActor = getFirstSourceActorForGeneratedSource( 426 getState(), 427 location.source.id 428 ); 429 location = createLocation({ ...location, source, sourceActor }); 430 } else if (!hasSourceActor(getState(), location.sourceActor.id)) { 431 // If the specific source actor no longer exists, match any still available. 432 const sourceActor = getFirstSourceActorForGeneratedSource( 433 getState(), 434 location.source.id 435 ); 436 location = createLocation({ ...location, sourceActor }); 437 } 438 await dispatch(selectSpecificLocation(location)); 439 return true; 440 }; 441 } 442 443 /** 444 * Select the "mapped location". 445 * 446 * If the passed location is on a generated source, select the 447 * related location in the original source. 448 * If the passed location is on an original source, select the 449 * related location in the generated source. 450 */ 451 export function jumpToMappedLocation(location) { 452 return async function (thunkArgs) { 453 const { client, dispatch } = thunkArgs; 454 if (!client) { 455 return null; 456 } 457 458 // Map to either an original or a generated source location 459 const pairedLocation = await getRelatedMapLocation(location, thunkArgs); 460 461 // If we are on a non-mapped source, this will return the same location 462 // so ignore the request. 463 if (pairedLocation == location) { 464 return null; 465 } 466 467 return dispatch(selectSpecificLocation(pairedLocation)); 468 }; 469 } 470 471 export function jumpToMappedSelectedLocation() { 472 return async function ({ dispatch, getState }) { 473 const location = getSelectedLocation(getState()); 474 if (!location) { 475 return; 476 } 477 478 await dispatch(jumpToMappedLocation(location)); 479 }; 480 }