tor-browser

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

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 }