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 }