tor-browser

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

sources-tree.js (27759B)


      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 * Sources tree reducer
      7 *
      8 * A Source Tree is composed of:
      9 *
     10 *  - Thread Items To designate targets/threads.
     11 *    These are the roots of the Tree if no project directory is selected.
     12 *
     13 *  - Group Items To designates the different domains used in the website.
     14 *    These are direct children of threads and may contain directory or source items.
     15 *
     16 *  - Directory Items To designate all the folders.
     17 *    Note that each every folder has an items. The Source Tree React component is doing the magic to coallesce folders made of only one sub folder.
     18 *
     19 *  - Source Items To designate sources.
     20 *    They are the leaves of the Tree. (we should not have empty directories.)
     21 */
     22 
     23 const IGNORED_URLS = ["debugger eval code", "XStringBundle"];
     24 const IGNORED_EXTENSIONS = ["css", "svg", "png"];
     25 import { getRawSourceURL } from "../utils/source";
     26 import { prefs } from "../utils/prefs";
     27 import { getDisplayURL } from "../utils/sources-tree/getURL";
     28 
     29 import TargetCommand from "resource://devtools/shared/commands/target/target-command.js";
     30 
     31 const lazy = {};
     32 ChromeUtils.defineESModuleGetters(lazy, {
     33  BinarySearch: "resource://gre/modules/BinarySearch.sys.mjs",
     34 });
     35 
     36 export function initialSourcesTreeState({
     37  isWebExtension,
     38  mainThreadProjectDirectoryRoots = {},
     39 } = {}) {
     40  return {
     41    // List of all Thread Tree Items.
     42    // All other item types are children of these and aren't store in
     43    // the reducer as top level objects.
     44    threadItems: [],
     45 
     46    // List of `uniquePath` of Tree Items that are expanded.
     47    // This should be all but Source Tree Items.
     48    expanded: new Set(),
     49 
     50    // Reference to the currently focused Tree Item.
     51    // It can be any type of Tree Item.
     52    focusedItem: null,
     53 
     54    // Persisted main thread project roots by origin.
     55    // These will be applied whenever a new main thread is added.
     56    mainThreadProjectDirectoryRoots,
     57 
     58    // Project root set from the Source Tree.
     59    // This focuses the source tree on a subset of sources.
     60    projectDirectoryRoot: "",
     61 
     62    // The name is displayed in Source Tree header
     63    projectDirectoryRootName: "",
     64 
     65    // The full name is displayed in the Source Tree header's tooltip
     66    projectDirectoryRootFullName: "",
     67 
     68    // Reports if the debugged context is a web extension.
     69    // If so, we should display all web extension sources.
     70    isWebExtension,
     71 
     72    /**
     73     * Boolean, to be set to true in order to display WebExtension's content scripts
     74     * that are applied to the current page we are debugging.
     75     *
     76     * Covered by: browser_dbg-content-script-sources.js
     77     * Bound to: devtools.debugger.show-content-scripts
     78     *
     79     */
     80    showContentScripts: prefs.showContentScripts,
     81 
     82    mutableExtensionSources: [],
     83  };
     84 }
     85 
     86 // eslint-disable-next-line complexity
     87 export default function update(state = initialSourcesTreeState(), action) {
     88  switch (action.type) {
     89    case "SHOW_CONTENT_SCRIPTS": {
     90      const { shouldShow } = action;
     91      if (shouldShow !== state.showExtensionSources) {
     92        prefs.showContentScripts = shouldShow;
     93        return { ...state, showContentScripts: shouldShow };
     94      }
     95      return state;
     96    }
     97    case "ADD_ORIGINAL_SOURCES": {
     98      const { generatedSourceActor } = action;
     99      const validOriginalSources = action.originalSources.filter(source => {
    100        if (source.isExtension) {
    101          state.mutableExtensionSources.push({
    102            source,
    103            sourceActor: generatedSourceActor,
    104          });
    105        }
    106        return isSourceVisibleInSourceTree(
    107          source,
    108          state.showContentScripts,
    109          state.isWebExtension
    110        );
    111      });
    112      if (!validOriginalSources.length) {
    113        return state;
    114      }
    115      let changed = false;
    116      // Fork the array only once for all the sources
    117      const threadItems = [...state.threadItems];
    118      for (const source of validOriginalSources) {
    119        changed |= addSource(threadItems, source, generatedSourceActor);
    120      }
    121      if (changed) {
    122        return {
    123          ...state,
    124          threadItems,
    125        };
    126      }
    127      return state;
    128    }
    129    case "INSERT_SOURCE_ACTORS": {
    130      // With this action, we only cover generated sources.
    131      // (i.e. we need something else for sourcemapped/original sources)
    132      // But we do want to process source actors in order to be able to display
    133      // distinct Source Tree Items for sources with the same URL loaded in distinct thread.
    134      // (And may be also later be able to highlight the many sources with the same URL loaded in a given thread)
    135      const newSourceActors = action.sourceActors.filter(sourceActor => {
    136        if (sourceActor.sourceObject.isExtension) {
    137          state.mutableExtensionSources.push({
    138            source: sourceActor.sourceObject,
    139            sourceActor,
    140          });
    141        }
    142        return isSourceVisibleInSourceTree(
    143          sourceActor.sourceObject,
    144          state.showContentScripts,
    145          state.isWebExtension
    146        );
    147      });
    148      if (!newSourceActors.length) {
    149        return state;
    150      }
    151      let changed = false;
    152      // Fork the array only once for all the sources
    153      const threadItems = [...state.threadItems];
    154      for (const sourceActor of newSourceActors) {
    155        // We mostly wanted to read the thread of the SourceActor,
    156        // most of the interesting attributes are on the Source Object.
    157        changed |= addSource(
    158          threadItems,
    159          sourceActor.sourceObject,
    160          sourceActor
    161        );
    162      }
    163      if (changed) {
    164        return {
    165          ...state,
    166          threadItems,
    167        };
    168      }
    169      return state;
    170    }
    171 
    172    case "INSERT_THREAD":
    173      state = { ...state };
    174      addThread(state, action.newThread);
    175      return applyMainThreadProjectDirectoryRoot(state, action.newThread);
    176 
    177    case "REMOVE_THREAD": {
    178      const { threadActorID } = action;
    179      const index = state.threadItems.findIndex(item => {
    180        return item.threadActorID == threadActorID;
    181      });
    182 
    183      if (index == -1) {
    184        return state;
    185      }
    186 
    187      // Also clear focusedItem and expanded items related
    188      // to this thread. These fields store uniquePath which starts
    189      // with the thread actor ID.
    190      let { focusedItem } = state;
    191      if (focusedItem && focusedItem.uniquePath.startsWith(threadActorID)) {
    192        focusedItem = null;
    193      }
    194      const expanded = new Set();
    195      for (const path of state.expanded) {
    196        if (!path.startsWith(threadActorID)) {
    197          expanded.add(path);
    198        }
    199      }
    200 
    201      // clear the project root if it was set to this thread
    202      let {
    203        projectDirectoryRoot,
    204        projectDirectoryRootName,
    205        projectDirectoryRootFullName,
    206      } = state;
    207      if (projectDirectoryRoot.startsWith(`${threadActorID}|`)) {
    208        projectDirectoryRoot = "";
    209        projectDirectoryRootName = "";
    210        projectDirectoryRootFullName = "";
    211      }
    212 
    213      const threadItems = [...state.threadItems];
    214      threadItems.splice(index, 1);
    215      return {
    216        ...state,
    217        threadItems,
    218        focusedItem,
    219        expanded,
    220        projectDirectoryRoot,
    221        projectDirectoryRootName,
    222        projectDirectoryRootFullName,
    223      };
    224    }
    225 
    226    case "SET_EXPANDED_STATE":
    227      return updateExpanded(state, action);
    228 
    229    case "SET_FOCUSED_SOURCE_ITEM":
    230      return { ...state, focusedItem: action.item };
    231 
    232    case "SET_SELECTED_LOCATION":
    233      return updateSelectedLocation(state, action.location);
    234 
    235    case "SET_PROJECT_DIRECTORY_ROOT": {
    236      const { uniquePath, name, fullName, mainThread } = action;
    237      return updateProjectDirectoryRoot(
    238        state,
    239        uniquePath,
    240        name,
    241        fullName,
    242        mainThread
    243      );
    244    }
    245 
    246    case "BLACKBOX_WHOLE_SOURCES":
    247    case "BLACKBOX_SOURCE_RANGES": {
    248      const sources = action.sources || [action.source];
    249      return updateBlackbox(state, sources, true);
    250    }
    251 
    252    case "UNBLACKBOX_WHOLE_SOURCES": {
    253      const sources = action.sources || [action.source];
    254      return updateBlackbox(state, sources, false);
    255    }
    256  }
    257  return state;
    258 }
    259 
    260 function addThread(state, thread) {
    261  const threadActorID = thread.actor;
    262  let threadItem = state.threadItems.find(item => {
    263    return item.threadActorID == threadActorID;
    264  });
    265  if (!threadItem) {
    266    threadItem = createThreadTreeItem(threadActorID);
    267    state.threadItems = [...state.threadItems];
    268    threadItem.thread = thread;
    269    addSortedItem(state.threadItems, threadItem, sortThreadItems);
    270  } else {
    271    // We force updating the list to trigger mapStateToProps
    272    // as the getSourcesTreeSources selector is awaiting for the `thread` attribute
    273    // which we will set here.
    274    state.threadItems = [...state.threadItems];
    275 
    276    // Inject the reducer thread object on Thread Tree Items
    277    // (this is handy shortcut to have access to from React components)
    278    // (this is also used by sortThreadItems to sort the thread as a Tree in the Browser Toolbox)
    279    threadItem.thread = thread;
    280 
    281    // We have to remove and re-insert the thread as its order will be based on the newly set `thread` attribute
    282    state.threadItems = [...state.threadItems];
    283    state.threadItems.splice(state.threadItems.indexOf(threadItem), 1);
    284    addSortedItem(state.threadItems, threadItem, sortThreadItems);
    285  }
    286 }
    287 
    288 function updateBlackbox(state, sources, shouldBlackBox) {
    289  const threadItems = [...state.threadItems];
    290 
    291  for (const source of sources) {
    292    for (const threadItem of threadItems) {
    293      const sourceTreeItem = findSourceInThreadItem(source, threadItem);
    294      if (sourceTreeItem && sourceTreeItem.isBlackBoxed != shouldBlackBox) {
    295        // Replace the Item with a clone so that we get the expected React updates
    296        const { children } = sourceTreeItem.parent;
    297        children.splice(children.indexOf(sourceTreeItem), 1, {
    298          ...sourceTreeItem,
    299          isBlackBoxed: shouldBlackBox,
    300        });
    301        threadItem.children = [...threadItem.children];
    302      }
    303    }
    304  }
    305  return { ...state, threadItems };
    306 }
    307 
    308 function updateExpanded(state, action) {
    309  // We receive the full list of all expanded items
    310  // (not only the one added/removed)
    311  // Also assume that this action is called only if the Set changed.
    312  return {
    313    ...state,
    314    // Consider that the action already cloned the Set
    315    expanded: action.expanded,
    316  };
    317 }
    318 
    319 /**
    320 * Update the project directory root
    321 */
    322 function updateProjectDirectoryRoot(
    323  state,
    324  uniquePath,
    325  name,
    326  fullName,
    327  mainThread
    328 ) {
    329  let directoryRoots = state.mainThreadProjectDirectoryRoots;
    330  if (mainThread) {
    331    const { origin } = getDisplayURL(mainThread.url);
    332    if (origin) {
    333      // Update the persisted main thread project directory root for this origin
    334      directoryRoots = { ...directoryRoots };
    335      if (uniquePath.startsWith(`${mainThread.actor}|`)) {
    336        directoryRoots[origin] = {
    337          // uniquePath contains the thread actor name, origin and path,
    338          // e.g. "server0.conn0.watcher2.process6//thread1|example.com|/src/"
    339          // We remove the thread actor name and re-add it when
    340          // applying this directory root to another thread because
    341          // the new thread will in general have a different name
    342          uniquePath: uniquePath.substring(mainThread.actor.length),
    343          name,
    344          fullName,
    345        };
    346      } else {
    347        // The directory root is set to a thread other than the main thread,
    348        // we don't persist it in this case because there is no reliable way
    349        // to identify this thread after reloading
    350        delete directoryRoots[origin];
    351      }
    352    }
    353  }
    354 
    355  return {
    356    ...state,
    357    mainThreadProjectDirectoryRoots: directoryRoots,
    358    projectDirectoryRoot: uniquePath,
    359    projectDirectoryRootName: name,
    360    projectDirectoryRootFullName: fullName,
    361  };
    362 }
    363 
    364 function applyMainThreadProjectDirectoryRoot(state, thread) {
    365  if (!thread.isTopLevel || !thread.url) {
    366    return state;
    367  }
    368  const { origin } = getDisplayURL(thread.url);
    369  if (!origin) {
    370    return state;
    371  }
    372 
    373  const directoryRoot = state.mainThreadProjectDirectoryRoots[origin];
    374  const uniquePath = directoryRoot
    375    ? thread.actor + directoryRoot.uniquePath
    376    : "";
    377  const name = directoryRoot?.name ?? "";
    378  const fullName = directoryRoot?.fullName ?? "";
    379 
    380  return {
    381    ...state,
    382    projectDirectoryRoot: uniquePath,
    383    projectDirectoryRootName: name,
    384    projectDirectoryRootFullName: fullName,
    385  };
    386 }
    387 
    388 function isSourceVisibleInSourceTree(
    389  source,
    390  showContentScripts,
    391  debuggeeIsWebExtension
    392 ) {
    393  return (
    394    !!source.url &&
    395    !IGNORED_EXTENSIONS.includes(source.displayURL.fileExtension) &&
    396    !IGNORED_URLS.includes(source.url) &&
    397    !source.isPrettyPrinted &&
    398    // Only accept web extension sources when the chrome pref is enabled (to allows showing content scripts),
    399    // or when we are debugging an extension
    400    (!source.isExtension || showContentScripts || debuggeeIsWebExtension)
    401  );
    402 }
    403 
    404 /**
    405 * Generic Array helper to add a new value at the right position
    406 * given that the array is already sorted.
    407 *
    408 * @param {Array} array
    409 *        The already sorted into which a value should be added.
    410 * @param {any} newValue
    411 *        The value to add in the array while keeping the array sorted.
    412 * @param {Function} comparator
    413 *        A function to compare two array values and their ordering.
    414 *        Follow same behavior as Array sorting function.
    415 */
    416 function addSortedItem(array, newValue, comparator) {
    417  const index = lazy.BinarySearch.insertionIndexOf(comparator, array, newValue);
    418  array.splice(index, 0, newValue);
    419 }
    420 
    421 // Cache each of last possible containers to speedup item addition
    422 // when we are adding to the same container (thread, group, folder)
    423 let lastThreadItem = null;
    424 let lastGroupItem = null;
    425 let lastDirectoryItem = null;
    426 
    427 function addSource(threadItems, source, sourceActor) {
    428  // Ensure creating or fetching the related Thread Item
    429  let threadItem;
    430  if (lastThreadItem?.threadActorID == sourceActor.thread) {
    431    threadItem = lastThreadItem;
    432  } else {
    433    threadItem = threadItems.find(item => {
    434      return item.threadActorID == sourceActor.thread;
    435    });
    436    if (!threadItem) {
    437      threadItem = createThreadTreeItem(sourceActor.thread);
    438      // Note that threadItems will be cloned once to force a state update
    439      // by the callsite of `addSourceActor`
    440      addSortedItem(threadItems, threadItem, sortThreadItems);
    441    }
    442    lastThreadItem = threadItem;
    443    lastGroupItem = null;
    444    lastDirectoryItem = null;
    445  }
    446 
    447  // Then ensure creating or fetching the related Group Item
    448  // About `source` versus `sourceActor`:
    449  const { displayURL } = source;
    450  const { group, origin } = displayURL;
    451 
    452  let groupItem;
    453  if (lastGroupItem?.groupName == group) {
    454    groupItem = lastGroupItem;
    455  } else {
    456    groupItem = threadItem.children.find(item => {
    457      return item.groupName == group;
    458    });
    459 
    460    if (!groupItem) {
    461      groupItem = createGroupTreeItem(group, origin, threadItem, source);
    462      // Copy children in order to force updating react in case we picked
    463      // this directory as a project root
    464      threadItem.children = [...threadItem.children];
    465 
    466      addSortedItem(threadItem.children, groupItem, sortItems);
    467    }
    468    lastGroupItem = groupItem;
    469    lastDirectoryItem = null;
    470  }
    471 
    472  // Then ensure creating or fetching all possibly nested Directory Item(s)
    473  const { path } = displayURL;
    474  const parentPath = path.substring(0, path.lastIndexOf("/"));
    475  let directoryItem;
    476  if (lastDirectoryItem?.path == parentPath) {
    477    directoryItem = lastDirectoryItem;
    478  } else {
    479    directoryItem = addOrGetParentDirectory(groupItem, parentPath);
    480    lastDirectoryItem = directoryItem;
    481  }
    482 
    483  // Check if a previous source actor registered this source.
    484  // It happens if we load the same url multiple times, or,
    485  // for inline sources (=HTML pages with inline scripts).
    486  const existing = directoryItem.children.find(item => {
    487    return item.type == "source" && item.source == source;
    488  });
    489  if (existing) {
    490    return false;
    491  }
    492 
    493  // Finaly, create the Source Item and register it in its parent Directory Item
    494  const sourceItem = createSourceTreeItem(source, sourceActor, directoryItem);
    495  // Copy children in order to force updating react in case we picked
    496  // this directory as a project root
    497  directoryItem.children = [...directoryItem.children];
    498  addSortedItem(directoryItem.children, sourceItem, sortItems);
    499 
    500  return true;
    501 }
    502 /**
    503 * Find all the source items in tree
    504 *
    505 * @param {object} item - Current item node in the tree
    506 * @param {Function} callback
    507 */
    508 function findSourceInThreadItem(source, threadItem) {
    509  const { displayURL } = source;
    510  const { group, path } = displayURL;
    511  const groupItem = threadItem.children.find(item => {
    512    return item.groupName == group;
    513  });
    514  if (!groupItem) {
    515    return null;
    516  }
    517 
    518  const parentPath = path.substring(0, path.lastIndexOf("/"));
    519 
    520  // If the parent path is empty, the source isn't in a sub directory,
    521  // and instead is an immediate child of the group item.
    522  if (!parentPath) {
    523    return groupItem.children.find(item => {
    524      return item.type == "source" && item.source == source;
    525    });
    526  }
    527 
    528  const directoryItem = groupItem._allGroupDirectoryItems.get(parentPath);
    529  if (!directoryItem) {
    530    return null;
    531  }
    532 
    533  return directoryItem.children.find(item => {
    534    return item.type == "source" && item.source == source;
    535  });
    536 }
    537 
    538 function sortItems(a, b) {
    539  if (a.type == "directory" && b.type == "source") {
    540    return -1;
    541  } else if (b.type == "directory" && a.type == "source") {
    542    return 1;
    543  } else if (a.type == "group" && b.type == "group") {
    544    return a.groupName.localeCompare(b.groupName);
    545  } else if (a.type == "directory" && b.type == "directory") {
    546    return a.path.localeCompare(b.path);
    547  } else if (a.type == "source" && b.type == "source") {
    548    return a.source.longName.localeCompare(b.source.longName);
    549  }
    550  return 0;
    551 }
    552 
    553 const { TYPES } = TargetCommand;
    554 const TARGET_TYPE_ORDER = [
    555  TYPES.PROCESS,
    556  TYPES.FRAME,
    557  TYPES.CONTENT_SCRIPT,
    558  TYPES.SERVICE_WORKER,
    559  TYPES.SHARED_WORKER,
    560  TYPES.WORKER,
    561 ];
    562 function sortThreadItems(threadItemA, threadItemB) {
    563  // Jest tests aren't emitting the necessary actions to populate the thread attributes.
    564  // Ignore sorting for them.
    565  if (!threadItemA.thread || !threadItemB.thread) {
    566    return 0;
    567  }
    568  return sortThreads(threadItemA.thread, threadItemB.thread);
    569 }
    570 export function sortThreads(a, b) {
    571  // Top level target is always listed first
    572  if (a.isTopLevel) {
    573    return -1;
    574  } else if (b.isTopLevel) {
    575    return 1;
    576  }
    577 
    578  // Order frame and content script per Window Global ID.
    579  // It should order them by creation date.
    580  if (a.innerWindowId > b.innerWindowId) {
    581    return 1;
    582  } else if (a.innerWindowId < b.innerWindowId) {
    583    return -1;
    584  }
    585 
    586  // If the two target have a different type, order by type
    587  if (a.targetType !== b.targetType) {
    588    const idxA = TARGET_TYPE_ORDER.indexOf(a.targetType);
    589    const idxB = TARGET_TYPE_ORDER.indexOf(b.targetType);
    590    return idxA < idxB ? -1 : 1;
    591  }
    592 
    593  // Order the process targets by their process ids
    594  if (a.processID && b.processID) {
    595    if (a.processID > b.processID) {
    596      return 1;
    597    } else if (a.processID < b.processID) {
    598      return -1;
    599    }
    600  }
    601 
    602  // Order the frame, worker and content script targets by their target name
    603  if (
    604    (a.targetType == "frame" && b.targetType == "frame") ||
    605    (a.targetType.endsWith("worker") && b.targetType.endsWith("worker")) ||
    606    (a.targetType == "content_script" && b.targetType == "content_script")
    607  ) {
    608    return a.name.localeCompare(b.name);
    609  }
    610 
    611  return 0;
    612 }
    613 
    614 /**
    615 * For a given URL's path, in the given group (i.e. typically a given scheme+domain),
    616 * return the already existing parent directory item, or create it if it doesn't exists.
    617 * Note that it will create all ancestors up to the Group Item.
    618 *
    619 * @param {GroupItem} groupItem
    620 *        The Group Item for the group where the path should be displayed.
    621 * @param {string} path
    622 *        Path of the directory for which we want a Directory Item.
    623 * @return {GroupItem|DirectoryItem}
    624 *        The parent Item where this path should be inserted.
    625 *        Note that it may be displayed right under the Group Item if the path is empty.
    626 */
    627 function addOrGetParentDirectory(groupItem, path) {
    628  // We reached the top of the Tree, so return the Group Item.
    629  if (!path) {
    630    return groupItem;
    631  }
    632  // See if we have this directory already registered by a previous source
    633  const existing = groupItem._allGroupDirectoryItems.get(path);
    634  if (existing) {
    635    return existing;
    636  }
    637  // It doesn't exists, so we will create a new Directory Item.
    638  // But now, lookup recursively for the parent Item for this to-be-create Directory Item
    639  const parentPath = path.substring(0, path.lastIndexOf("/"));
    640  const parentDirectory = addOrGetParentDirectory(groupItem, parentPath);
    641 
    642  // We can now create the new Directory Item and register it in its parent Item.
    643  const directory = createDirectoryTreeItem(path, parentDirectory);
    644  // Copy children in order to force updating react in case we picked
    645  // this directory as a project root
    646  parentDirectory.children = [...parentDirectory.children];
    647 
    648  addSortedItem(parentDirectory.children, directory, sortItems);
    649 
    650  // Also maintain the list of all group items,
    651  // Which helps speedup querying for existing items.
    652  groupItem._allGroupDirectoryItems.set(directory.path, directory);
    653 
    654  return directory;
    655 }
    656 
    657 /**
    658 * Definition of all Items of a SourceTree
    659 */
    660 // Highlights the attributes that all Source Tree Item should expose
    661 function createBaseTreeItem({ type, parent, uniquePath, children }) {
    662  return {
    663    // Can be: thread, group, directory or source
    664    type,
    665    // Reference to the parent TreeItem
    666    parent,
    667    // This attribute is used for two things:
    668    // * as a string key identified in the React Tree
    669    // * for project root in order to find the root in the tree
    670    // It is of the form:
    671    // `${ThreadActorID}|${GroupName}|${DirectoryPath}|${SourceID}`
    672    // Group and path/ID are optional.
    673    // `|` is used as separator in order to avoid having this character being used in name/path/IDs.
    674    uniquePath,
    675    // Array of TreeItem, children of this item.
    676    // Will be null for Source Tree Item
    677    children,
    678  };
    679 }
    680 function createThreadTreeItem(thread) {
    681  return {
    682    ...createBaseTreeItem({
    683      type: "thread",
    684      // Each thread is considered as an independant root item
    685      parent: null,
    686      uniquePath: thread,
    687      // Children of threads will only be Group Items
    688      children: [],
    689    }),
    690 
    691    // This will be used to set the reducer's thread object.
    692    // This threadActorID attribute isn't meant to be used outside of this selector.
    693    // A `thread` attribute will be exposed from INSERT_THREAD action.
    694    threadActorID: thread,
    695  };
    696 }
    697 function createGroupTreeItem(groupName, origin, parent, source) {
    698  return {
    699    ...createBaseTreeItem({
    700      type: "group",
    701      parent,
    702      uniquePath: `${parent.uniquePath}|${groupName}`,
    703      // Children of Group can be Directory and Source items
    704      children: [],
    705    }),
    706 
    707    groupName,
    708 
    709    // This is only used by project directory root tooltip
    710    origin,
    711 
    712    // When a content script appear in a web page,
    713    // a dedicated group is created for it and should
    714    // be having an extension icon.
    715    isForExtensionSource: source.isExtension,
    716 
    717    // Map of all nested directory items for this group, keyed by their path.
    718    // This helps find any nested directory in a given group without having to walk the tree.
    719    // This is meant to be used only within the reducer.
    720    _allGroupDirectoryItems: new Map(),
    721  };
    722 }
    723 function createDirectoryTreeItem(path, parent) {
    724  // If the parent is a group we want to use '/' as separator
    725  const pathSeparator = parent.type == "directory" ? "/" : "|";
    726 
    727  // `path` will be the absolute path from the group/domain,
    728  // while we want to append only the directory name in uniquePath.
    729  // Also, we need to strip '/' prefix.
    730  const relativePath =
    731    parent.type == "directory"
    732      ? path.replace(parent.path, "").replace(/^\//, "")
    733      : path;
    734 
    735  return {
    736    ...createBaseTreeItem({
    737      type: "directory",
    738      parent,
    739      uniquePath: `${parent.uniquePath}${pathSeparator}${relativePath}`,
    740      // Children can be nested Directory or Source items
    741      children: [],
    742    }),
    743 
    744    // This is the absolute path from the "group"
    745    // i.e. the path from the domain name
    746    // For http://mozilla.org/foo/bar folder,
    747    // path will be:
    748    //   foo/bar
    749    path,
    750  };
    751 }
    752 function createSourceTreeItem(source, sourceActor, parent) {
    753  return {
    754    ...createBaseTreeItem({
    755      type: "source",
    756      parent,
    757      uniquePath: `${parent.uniquePath}|${source.id}`,
    758      // Sources items are leaves of the SourceTree
    759      children: null,
    760    }),
    761 
    762    source,
    763    sourceActor,
    764  };
    765 }
    766 
    767 /**
    768 * Update `expanded` and `focusedItem` so that we show and focus
    769 * the new selected source.
    770 *
    771 * @param {object} state
    772 * @param {object} selectedLocation
    773 *        The new location being selected.
    774 */
    775 function updateSelectedLocation(state, selectedLocation) {
    776  const sourceItem = getSourceItemForSelectedLocation(state, selectedLocation);
    777  if (sourceItem) {
    778    // Walk up the tree to expand all ancestor items up to the root of the tree.
    779    const expanded = new Set(state.expanded);
    780    let parentDirectory = sourceItem;
    781    while (parentDirectory) {
    782      expanded.add(parentDirectory.uniquePath);
    783      parentDirectory = parentDirectory.parent;
    784    }
    785 
    786    return {
    787      ...state,
    788      expanded,
    789      focusedItem: sourceItem,
    790    };
    791  }
    792  return state;
    793 }
    794 
    795 /**
    796 * Get the SourceItem displayed in the SourceTree for the currently selected location.
    797 *
    798 * @param {object} state
    799 * @param {object} selectedLocation
    800 * @return {SourceItem}
    801 *        The directory source item where the given source is displayed.
    802 */
    803 function getSourceItemForSelectedLocation(state, selectedLocation) {
    804  const { source, sourceActor } = selectedLocation;
    805 
    806  // Sources without URLs are not visible in the SourceTree
    807  if (!source.url) {
    808    return null;
    809  }
    810 
    811  // In the SourceTree, we never show the pretty printed sources and only
    812  // the minified version, so if we are selecting a pretty file, fake selecting
    813  // the minified version by looking up for the minified URL instead of the pretty one.
    814  const sourceUrl = getRawSourceURL(source.url);
    815 
    816  const { displayURL } = source;
    817  function findSourceInItem(item, path) {
    818    if (item.type == "source") {
    819      if (item.source.url == sourceUrl) {
    820        return item;
    821      }
    822      return null;
    823    }
    824    // Bail out if the current item doesn't match the source
    825    if (item.type == "thread" && item.threadActorID != sourceActor?.thread) {
    826      return null;
    827    }
    828    if (item.type == "group" && displayURL.group != item.groupName) {
    829      return null;
    830    }
    831    if (item.type == "directory" && !path.startsWith(item.path)) {
    832      return null;
    833    }
    834    // Otherwise, walk down the tree if this ancestor item seems to match
    835    for (const child of item.children) {
    836      const match = findSourceInItem(child, path);
    837      if (match) {
    838        return match;
    839      }
    840    }
    841 
    842    return null;
    843  }
    844  for (const rootItem of state.threadItems) {
    845    // Note that when we are setting a project root, rootItem
    846    // may no longer be only Thread Item, but also be Group, Directory or Source Items.
    847    const item = findSourceInItem(rootItem, displayURL.path);
    848    if (item) {
    849      return item;
    850    }
    851  }
    852  return null;
    853 }