"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.initialSourcesTreeState = initialSourcesTreeState;
exports.default = update;
exports.sortThreads = sortThreads;
loader.lazyRequireGetter(this, "_source", "devtools/client/debugger/src/utils/source");
loader.lazyRequireGetter(this, "_prefs", "devtools/client/debugger/src/utils/prefs");
loader.lazyRequireGetter(this, "_getURL", "devtools/client/debugger/src/utils/sources-tree/getURL");

var _targetCommand = _interopRequireDefault(require("resource://devtools/shared/commands/target/target-command.js"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */

/**
 * Sources tree reducer
 *
 * A Source Tree is composed of:
 *
 *  - Thread Items To designate targets/threads.
 *    These are the roots of the Tree if no project directory is selected.
 *
 *  - Group Items To designates the different domains used in the website.
 *    These are direct children of threads and may contain directory or source items.
 *
 *  - Directory Items To designate all the folders.
 *    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.
 *
 *  - Source Items To designate sources.
 *    They are the leaves of the Tree. (we should not have empty directories.)
 */
const IGNORED_URLS = ["debugger eval code", "XStringBundle"];
const IGNORED_EXTENSIONS = ["css", "svg", "png"];
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  BinarySearch: "resource://gre/modules/BinarySearch.sys.mjs"
});

function initialSourcesTreeState({
  isWebExtension,
  mainThreadProjectDirectoryRoots = {}
} = {}) {
  return {
    // List of all Thread Tree Items.
    // All other item types are children of these and aren't store in
    // the reducer as top level objects.
    threadItems: [],
    // List of `uniquePath` of Tree Items that are expanded.
    // This should be all but Source Tree Items.
    expanded: new Set(),
    // Reference to the currently focused Tree Item.
    // It can be any type of Tree Item.
    focusedItem: null,
    // Persisted main thread project roots by origin.
    // These will be applied whenever a new main thread is added.
    mainThreadProjectDirectoryRoots,
    // Project root set from the Source Tree.
    // This focuses the source tree on a subset of sources.
    projectDirectoryRoot: "",
    // The name is displayed in Source Tree header
    projectDirectoryRootName: "",
    // The full name is displayed in the Source Tree header's tooltip
    projectDirectoryRootFullName: "",
    // Reports if the debugged context is a web extension.
    // If so, we should display all web extension sources.
    isWebExtension,

    /**
     * Boolean, to be set to true in order to display WebExtension's content scripts
     * that are applied to the current page we are debugging.
     *
     * Covered by: browser_dbg-content-script-sources.js
     * Bound to: devtools.debugger.show-content-scripts
     *
     */
    showContentScripts: _prefs.prefs.showContentScripts,
    mutableExtensionSources: []
  };
} // eslint-disable-next-line complexity


function update(state = initialSourcesTreeState(), action) {
  switch (action.type) {
    case "SHOW_CONTENT_SCRIPTS":
      {
        const {
          shouldShow
        } = action;

        if (shouldShow !== state.showExtensionSources) {
          _prefs.prefs.showContentScripts = shouldShow;
          return { ...state,
            showContentScripts: shouldShow
          };
        }

        return state;
      }

    case "ADD_ORIGINAL_SOURCES":
      {
        const {
          generatedSourceActor
        } = action;
        const validOriginalSources = action.originalSources.filter(source => {
          if (source.isExtension) {
            state.mutableExtensionSources.push({
              source,
              sourceActor: generatedSourceActor
            });
          }

          return isSourceVisibleInSourceTree(source, state.showContentScripts, state.isWebExtension);
        });

        if (!validOriginalSources.length) {
          return state;
        }

        let changed = false; // Fork the array only once for all the sources

        const threadItems = [...state.threadItems];

        for (const source of validOriginalSources) {
          changed |= addSource(threadItems, source, generatedSourceActor);
        }

        if (changed) {
          return { ...state,
            threadItems
          };
        }

        return state;
      }

    case "INSERT_SOURCE_ACTORS":
      {
        // With this action, we only cover generated sources.
        // (i.e. we need something else for sourcemapped/original sources)
        // But we do want to process source actors in order to be able to display
        // distinct Source Tree Items for sources with the same URL loaded in distinct thread.
        // (And may be also later be able to highlight the many sources with the same URL loaded in a given thread)
        const newSourceActors = action.sourceActors.filter(sourceActor => {
          if (sourceActor.sourceObject.isExtension) {
            state.mutableExtensionSources.push({
              source: sourceActor.sourceObject,
              sourceActor
            });
          }

          return isSourceVisibleInSourceTree(sourceActor.sourceObject, state.showContentScripts, state.isWebExtension);
        });

        if (!newSourceActors.length) {
          return state;
        }

        let changed = false; // Fork the array only once for all the sources

        const threadItems = [...state.threadItems];

        for (const sourceActor of newSourceActors) {
          // We mostly wanted to read the thread of the SourceActor,
          // most of the interesting attributes are on the Source Object.
          changed |= addSource(threadItems, sourceActor.sourceObject, sourceActor);
        }

        if (changed) {
          return { ...state,
            threadItems
          };
        }

        return state;
      }

    case "INSERT_THREAD":
      state = { ...state
      };
      addThread(state, action.newThread);
      return applyMainThreadProjectDirectoryRoot(state, action.newThread);

    case "REMOVE_THREAD":
      {
        const {
          threadActorID
        } = action;
        const index = state.threadItems.findIndex(item => {
          return item.threadActorID == threadActorID;
        });

        if (index == -1) {
          return state;
        } // Also clear focusedItem and expanded items related
        // to this thread. These fields store uniquePath which starts
        // with the thread actor ID.


        let {
          focusedItem
        } = state;

        if (focusedItem && focusedItem.uniquePath.startsWith(threadActorID)) {
          focusedItem = null;
        }

        const expanded = new Set();

        for (const path of state.expanded) {
          if (!path.startsWith(threadActorID)) {
            expanded.add(path);
          }
        } // clear the project root if it was set to this thread


        let {
          projectDirectoryRoot,
          projectDirectoryRootName,
          projectDirectoryRootFullName
        } = state;

        if (projectDirectoryRoot.startsWith(`${threadActorID}|`)) {
          projectDirectoryRoot = "";
          projectDirectoryRootName = "";
          projectDirectoryRootFullName = "";
        }

        const threadItems = [...state.threadItems];
        threadItems.splice(index, 1);
        return { ...state,
          threadItems,
          focusedItem,
          expanded,
          projectDirectoryRoot,
          projectDirectoryRootName,
          projectDirectoryRootFullName
        };
      }

    case "SET_EXPANDED_STATE":
      return updateExpanded(state, action);

    case "SET_FOCUSED_SOURCE_ITEM":
      return { ...state,
        focusedItem: action.item
      };

    case "SET_SELECTED_LOCATION":
      return updateSelectedLocation(state, action.location);

    case "SET_PROJECT_DIRECTORY_ROOT":
      {
        const {
          uniquePath,
          name,
          fullName,
          mainThread
        } = action;
        return updateProjectDirectoryRoot(state, uniquePath, name, fullName, mainThread);
      }

    case "BLACKBOX_WHOLE_SOURCES":
    case "BLACKBOX_SOURCE_RANGES":
      {
        const sources = action.sources || [action.source];
        return updateBlackbox(state, sources, true);
      }

    case "UNBLACKBOX_WHOLE_SOURCES":
      {
        const sources = action.sources || [action.source];
        return updateBlackbox(state, sources, false);
      }
  }

  return state;
}

function addThread(state, thread) {
  const threadActorID = thread.actor;
  let threadItem = state.threadItems.find(item => {
    return item.threadActorID == threadActorID;
  });

  if (!threadItem) {
    threadItem = createThreadTreeItem(threadActorID);
    state.threadItems = [...state.threadItems];
    threadItem.thread = thread;
    addSortedItem(state.threadItems, threadItem, sortThreadItems);
  } else {
    // We force updating the list to trigger mapStateToProps
    // as the getSourcesTreeSources selector is awaiting for the `thread` attribute
    // which we will set here.
    state.threadItems = [...state.threadItems]; // Inject the reducer thread object on Thread Tree Items
    // (this is handy shortcut to have access to from React components)
    // (this is also used by sortThreadItems to sort the thread as a Tree in the Browser Toolbox)

    threadItem.thread = thread; // We have to remove and re-insert the thread as its order will be based on the newly set `thread` attribute

    state.threadItems = [...state.threadItems];
    state.threadItems.splice(state.threadItems.indexOf(threadItem), 1);
    addSortedItem(state.threadItems, threadItem, sortThreadItems);
  }
}

function updateBlackbox(state, sources, shouldBlackBox) {
  const threadItems = [...state.threadItems];

  for (const source of sources) {
    for (const threadItem of threadItems) {
      const sourceTreeItem = findSourceInThreadItem(source, threadItem);

      if (sourceTreeItem && sourceTreeItem.isBlackBoxed != shouldBlackBox) {
        // Replace the Item with a clone so that we get the expected React updates
        const {
          children
        } = sourceTreeItem.parent;
        children.splice(children.indexOf(sourceTreeItem), 1, { ...sourceTreeItem,
          isBlackBoxed: shouldBlackBox
        });
        threadItem.children = [...threadItem.children];
      }
    }
  }

  return { ...state,
    threadItems
  };
}

function updateExpanded(state, action) {
  // We receive the full list of all expanded items
  // (not only the one added/removed)
  // Also assume that this action is called only if the Set changed.
  return { ...state,
    // Consider that the action already cloned the Set
    expanded: action.expanded
  };
}
/**
 * Update the project directory root
 */


function updateProjectDirectoryRoot(state, uniquePath, name, fullName, mainThread) {
  let directoryRoots = state.mainThreadProjectDirectoryRoots;

  if (mainThread) {
    const {
      origin
    } = (0, _getURL.getDisplayURL)(mainThread.url);

    if (origin) {
      // Update the persisted main thread project directory root for this origin
      directoryRoots = { ...directoryRoots
      };

      if (uniquePath.startsWith(`${mainThread.actor}|`)) {
        directoryRoots[origin] = {
          // uniquePath contains the thread actor name, origin and path,
          // e.g. "server0.conn0.watcher2.process6//thread1|example.com|/src/"
          // We remove the thread actor name and re-add it when
          // applying this directory root to another thread because
          // the new thread will in general have a different name
          uniquePath: uniquePath.substring(mainThread.actor.length),
          name,
          fullName
        };
      } else {
        // The directory root is set to a thread other than the main thread,
        // we don't persist it in this case because there is no reliable way
        // to identify this thread after reloading
        delete directoryRoots[origin];
      }
    }
  }

  return { ...state,
    mainThreadProjectDirectoryRoots: directoryRoots,
    projectDirectoryRoot: uniquePath,
    projectDirectoryRootName: name,
    projectDirectoryRootFullName: fullName
  };
}

function applyMainThreadProjectDirectoryRoot(state, thread) {
  if (!thread.isTopLevel || !thread.url) {
    return state;
  }

  const {
    origin
  } = (0, _getURL.getDisplayURL)(thread.url);

  if (!origin) {
    return state;
  }

  const directoryRoot = state.mainThreadProjectDirectoryRoots[origin];
  const uniquePath = directoryRoot ? thread.actor + directoryRoot.uniquePath : "";
  const name = directoryRoot?.name ?? "";
  const fullName = directoryRoot?.fullName ?? "";
  return { ...state,
    projectDirectoryRoot: uniquePath,
    projectDirectoryRootName: name,
    projectDirectoryRootFullName: fullName
  };
}

function isSourceVisibleInSourceTree(source, showContentScripts, debuggeeIsWebExtension) {
  return !!source.url && !IGNORED_EXTENSIONS.includes(source.displayURL.fileExtension) && !IGNORED_URLS.includes(source.url) && !source.isPrettyPrinted && ( // Only accept web extension sources when the chrome pref is enabled (to allows showing content scripts),
  // or when we are debugging an extension
  !source.isExtension || showContentScripts || debuggeeIsWebExtension);
}
/**
 * Generic Array helper to add a new value at the right position
 * given that the array is already sorted.
 *
 * @param {Array} array
 *        The already sorted into which a value should be added.
 * @param {any} newValue
 *        The value to add in the array while keeping the array sorted.
 * @param {Function} comparator
 *        A function to compare two array values and their ordering.
 *        Follow same behavior as Array sorting function.
 */


function addSortedItem(array, newValue, comparator) {
  const index = lazy.BinarySearch.insertionIndexOf(comparator, array, newValue);
  array.splice(index, 0, newValue);
} // Cache each of last possible containers to speedup item addition
// when we are adding to the same container (thread, group, folder)


let lastThreadItem = null;
let lastGroupItem = null;
let lastDirectoryItem = null;

function addSource(threadItems, source, sourceActor) {
  // Ensure creating or fetching the related Thread Item
  let threadItem;

  if (lastThreadItem?.threadActorID == sourceActor.thread) {
    threadItem = lastThreadItem;
  } else {
    threadItem = threadItems.find(item => {
      return item.threadActorID == sourceActor.thread;
    });

    if (!threadItem) {
      threadItem = createThreadTreeItem(sourceActor.thread); // Note that threadItems will be cloned once to force a state update
      // by the callsite of `addSourceActor`

      addSortedItem(threadItems, threadItem, sortThreadItems);
    }

    lastThreadItem = threadItem;
    lastGroupItem = null;
    lastDirectoryItem = null;
  } // Then ensure creating or fetching the related Group Item
  // About `source` versus `sourceActor`:


  const {
    displayURL
  } = source;
  const {
    group,
    origin
  } = displayURL;
  let groupItem;

  if (lastGroupItem?.groupName == group) {
    groupItem = lastGroupItem;
  } else {
    groupItem = threadItem.children.find(item => {
      return item.groupName == group;
    });

    if (!groupItem) {
      groupItem = createGroupTreeItem(group, origin, threadItem, source); // Copy children in order to force updating react in case we picked
      // this directory as a project root

      threadItem.children = [...threadItem.children];
      addSortedItem(threadItem.children, groupItem, sortItems);
    }

    lastGroupItem = groupItem;
    lastDirectoryItem = null;
  } // Then ensure creating or fetching all possibly nested Directory Item(s)


  const {
    path
  } = displayURL;
  const parentPath = path.substring(0, path.lastIndexOf("/"));
  let directoryItem;

  if (lastDirectoryItem?.path == parentPath) {
    directoryItem = lastDirectoryItem;
  } else {
    directoryItem = addOrGetParentDirectory(groupItem, parentPath);
    lastDirectoryItem = directoryItem;
  } // Check if a previous source actor registered this source.
  // It happens if we load the same url multiple times, or,
  // for inline sources (=HTML pages with inline scripts).


  const existing = directoryItem.children.find(item => {
    return item.type == "source" && item.source == source;
  });

  if (existing) {
    return false;
  } // Finaly, create the Source Item and register it in its parent Directory Item


  const sourceItem = createSourceTreeItem(source, sourceActor, directoryItem); // Copy children in order to force updating react in case we picked
  // this directory as a project root

  directoryItem.children = [...directoryItem.children];
  addSortedItem(directoryItem.children, sourceItem, sortItems);
  return true;
}
/**
 * Find all the source items in tree
 *
 * @param {object} item - Current item node in the tree
 * @param {Function} callback
 */


function findSourceInThreadItem(source, threadItem) {
  const {
    displayURL
  } = source;
  const {
    group,
    path
  } = displayURL;
  const groupItem = threadItem.children.find(item => {
    return item.groupName == group;
  });

  if (!groupItem) {
    return null;
  }

  const parentPath = path.substring(0, path.lastIndexOf("/")); // If the parent path is empty, the source isn't in a sub directory,
  // and instead is an immediate child of the group item.

  if (!parentPath) {
    return groupItem.children.find(item => {
      return item.type == "source" && item.source == source;
    });
  }

  const directoryItem = groupItem._allGroupDirectoryItems.get(parentPath);

  if (!directoryItem) {
    return null;
  }

  return directoryItem.children.find(item => {
    return item.type == "source" && item.source == source;
  });
}

function sortItems(a, b) {
  if (a.type == "directory" && b.type == "source") {
    return -1;
  } else if (b.type == "directory" && a.type == "source") {
    return 1;
  } else if (a.type == "group" && b.type == "group") {
    return a.groupName.localeCompare(b.groupName);
  } else if (a.type == "directory" && b.type == "directory") {
    return a.path.localeCompare(b.path);
  } else if (a.type == "source" && b.type == "source") {
    return a.source.longName.localeCompare(b.source.longName);
  }

  return 0;
}

const {
  TYPES
} = _targetCommand.default;
const TARGET_TYPE_ORDER = [TYPES.PROCESS, TYPES.FRAME, TYPES.CONTENT_SCRIPT, TYPES.SERVICE_WORKER, TYPES.SHARED_WORKER, TYPES.WORKER];

function sortThreadItems(threadItemA, threadItemB) {
  // Jest tests aren't emitting the necessary actions to populate the thread attributes.
  // Ignore sorting for them.
  if (!threadItemA.thread || !threadItemB.thread) {
    return 0;
  }

  return sortThreads(threadItemA.thread, threadItemB.thread);
}

function sortThreads(a, b) {
  // Top level target is always listed first
  if (a.isTopLevel) {
    return -1;
  } else if (b.isTopLevel) {
    return 1;
  } // Order frame and content script per Window Global ID.
  // It should order them by creation date.


  if (a.innerWindowId > b.innerWindowId) {
    return 1;
  } else if (a.innerWindowId < b.innerWindowId) {
    return -1;
  } // If the two target have a different type, order by type


  if (a.targetType !== b.targetType) {
    const idxA = TARGET_TYPE_ORDER.indexOf(a.targetType);
    const idxB = TARGET_TYPE_ORDER.indexOf(b.targetType);
    return idxA < idxB ? -1 : 1;
  } // Order the process targets by their process ids


  if (a.processID && b.processID) {
    if (a.processID > b.processID) {
      return 1;
    } else if (a.processID < b.processID) {
      return -1;
    }
  } // Order the frame, worker and content script targets by their target name


  if (a.targetType == "frame" && b.targetType == "frame" || a.targetType.endsWith("worker") && b.targetType.endsWith("worker") || a.targetType == "content_script" && b.targetType == "content_script") {
    return a.name.localeCompare(b.name);
  }

  return 0;
}
/**
 * For a given URL's path, in the given group (i.e. typically a given scheme+domain),
 * return the already existing parent directory item, or create it if it doesn't exists.
 * Note that it will create all ancestors up to the Group Item.
 *
 * @param {GroupItem} groupItem
 *        The Group Item for the group where the path should be displayed.
 * @param {string} path
 *        Path of the directory for which we want a Directory Item.
 * @return {GroupItem|DirectoryItem}
 *        The parent Item where this path should be inserted.
 *        Note that it may be displayed right under the Group Item if the path is empty.
 */


function addOrGetParentDirectory(groupItem, path) {
  // We reached the top of the Tree, so return the Group Item.
  if (!path) {
    return groupItem;
  } // See if we have this directory already registered by a previous source


  const existing = groupItem._allGroupDirectoryItems.get(path);

  if (existing) {
    return existing;
  } // It doesn't exists, so we will create a new Directory Item.
  // But now, lookup recursively for the parent Item for this to-be-create Directory Item


  const parentPath = path.substring(0, path.lastIndexOf("/"));
  const parentDirectory = addOrGetParentDirectory(groupItem, parentPath); // We can now create the new Directory Item and register it in its parent Item.

  const directory = createDirectoryTreeItem(path, parentDirectory); // Copy children in order to force updating react in case we picked
  // this directory as a project root

  parentDirectory.children = [...parentDirectory.children];
  addSortedItem(parentDirectory.children, directory, sortItems); // Also maintain the list of all group items,
  // Which helps speedup querying for existing items.

  groupItem._allGroupDirectoryItems.set(directory.path, directory);

  return directory;
}
/**
 * Definition of all Items of a SourceTree
 */
// Highlights the attributes that all Source Tree Item should expose


function createBaseTreeItem({
  type,
  parent,
  uniquePath,
  children
}) {
  return {
    // Can be: thread, group, directory or source
    type,
    // Reference to the parent TreeItem
    parent,
    // This attribute is used for two things:
    // * as a string key identified in the React Tree
    // * for project root in order to find the root in the tree
    // It is of the form:
    // `${ThreadActorID}|${GroupName}|${DirectoryPath}|${SourceID}`
    // Group and path/ID are optional.
    // `|` is used as separator in order to avoid having this character being used in name/path/IDs.
    uniquePath,
    // Array of TreeItem, children of this item.
    // Will be null for Source Tree Item
    children
  };
}

function createThreadTreeItem(thread) {
  return { ...createBaseTreeItem({
      type: "thread",
      // Each thread is considered as an independant root item
      parent: null,
      uniquePath: thread,
      // Children of threads will only be Group Items
      children: []
    }),
    // This will be used to set the reducer's thread object.
    // This threadActorID attribute isn't meant to be used outside of this selector.
    // A `thread` attribute will be exposed from INSERT_THREAD action.
    threadActorID: thread
  };
}

function createGroupTreeItem(groupName, origin, parent, source) {
  return { ...createBaseTreeItem({
      type: "group",
      parent,
      uniquePath: `${parent.uniquePath}|${groupName}`,
      // Children of Group can be Directory and Source items
      children: []
    }),
    groupName,
    // This is only used by project directory root tooltip
    origin,
    // When a content script appear in a web page,
    // a dedicated group is created for it and should
    // be having an extension icon.
    isForExtensionSource: source.isExtension,
    // Map of all nested directory items for this group, keyed by their path.
    // This helps find any nested directory in a given group without having to walk the tree.
    // This is meant to be used only within the reducer.
    _allGroupDirectoryItems: new Map()
  };
}

function createDirectoryTreeItem(path, parent) {
  // If the parent is a group we want to use '/' as separator
  const pathSeparator = parent.type == "directory" ? "/" : "|"; // `path` will be the absolute path from the group/domain,
  // while we want to append only the directory name in uniquePath.
  // Also, we need to strip '/' prefix.

  const relativePath = parent.type == "directory" ? path.replace(parent.path, "").replace(/^\//, "") : path;
  return { ...createBaseTreeItem({
      type: "directory",
      parent,
      uniquePath: `${parent.uniquePath}${pathSeparator}${relativePath}`,
      // Children can be nested Directory or Source items
      children: []
    }),
    // This is the absolute path from the "group"
    // i.e. the path from the domain name
    // For http://mozilla.org/foo/bar folder,
    // path will be:
    //   foo/bar
    path
  };
}

function createSourceTreeItem(source, sourceActor, parent) {
  return { ...createBaseTreeItem({
      type: "source",
      parent,
      uniquePath: `${parent.uniquePath}|${source.id}`,
      // Sources items are leaves of the SourceTree
      children: null
    }),
    source,
    sourceActor
  };
}
/**
 * Update `expanded` and `focusedItem` so that we show and focus
 * the new selected source.
 *
 * @param {object} state
 * @param {object} selectedLocation
 *        The new location being selected.
 */


function updateSelectedLocation(state, selectedLocation) {
  const sourceItem = getSourceItemForSelectedLocation(state, selectedLocation);

  if (sourceItem) {
    // Walk up the tree to expand all ancestor items up to the root of the tree.
    const expanded = new Set(state.expanded);
    let parentDirectory = sourceItem;

    while (parentDirectory) {
      expanded.add(parentDirectory.uniquePath);
      parentDirectory = parentDirectory.parent;
    }

    return { ...state,
      expanded,
      focusedItem: sourceItem
    };
  }

  return state;
}
/**
 * Get the SourceItem displayed in the SourceTree for the currently selected location.
 *
 * @param {object} state
 * @param {object} selectedLocation
 * @return {SourceItem}
 *        The directory source item where the given source is displayed.
 */


function getSourceItemForSelectedLocation(state, selectedLocation) {
  const {
    source,
    sourceActor
  } = selectedLocation; // Sources without URLs are not visible in the SourceTree

  if (!source.url) {
    return null;
  } // In the SourceTree, we never show the pretty printed sources and only
  // the minified version, so if we are selecting a pretty file, fake selecting
  // the minified version by looking up for the minified URL instead of the pretty one.


  const sourceUrl = (0, _source.getRawSourceURL)(source.url);
  const {
    displayURL
  } = source;

  function findSourceInItem(item, path) {
    if (item.type == "source") {
      if (item.source.url == sourceUrl) {
        return item;
      }

      return null;
    } // Bail out if the current item doesn't match the source


    if (item.type == "thread" && item.threadActorID != sourceActor?.thread) {
      return null;
    }

    if (item.type == "group" && displayURL.group != item.groupName) {
      return null;
    }

    if (item.type == "directory" && !path.startsWith(item.path)) {
      return null;
    } // Otherwise, walk down the tree if this ancestor item seems to match


    for (const child of item.children) {
      const match = findSourceInItem(child, path);

      if (match) {
        return match;
      }
    }

    return null;
  }

  for (const rootItem of state.threadItems) {
    // Note that when we are setting a project root, rootItem
    // may no longer be only Thread Item, but also be Group, Directory or Source Items.
    const item = findSourceInItem(rootItem, displayURL.path);

    if (item) {
      return item;
    }
  }

  return null;
}