sources.js (14753B)
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 reducer 7 * 8 * @module reducers/sources 9 */ 10 11 import { prefs } from "../utils/prefs"; 12 import { createPendingSelectedLocation } from "../utils/location"; 13 14 export const UNDEFINED_LOCATION = Symbol("Undefined location"); 15 export const NO_LOCATION = Symbol("No location"); 16 17 export function initialSourcesState() { 18 /* eslint sort-keys: "error" */ 19 return { 20 /** 21 * List of all breakpoint positions for all sources (generated and original). 22 * Map of source id (string) to dictionary object whose keys are line numbers 23 * and values of array of positions. 24 * A position is an object made with two attributes: 25 * location and generatedLocation. Both refering to breakpoint positions 26 * in original and generated sources. 27 * In case of generated source, the two location will be the same. 28 * 29 * Map(source id => Dictionary(int => array<Position>)) 30 */ 31 mutableBreakpointPositions: new Map(), 32 33 /** 34 * List of all breakable lines for original sources only. 35 * 36 * Map(source id => promise or array<int> : breakable line numbers>) 37 * 38 * The value can be a promise to indicate the lines are being loaded. 39 */ 40 mutableOriginalBreakableLines: new Map(), 41 42 /** 43 * Map of the source id's to one or more related original source id's 44 * Only generated sources which have related original sources will be maintained here. 45 * 46 * Map(source id => array<Original Source ID>) 47 */ 48 mutableOriginalSources: new Map(), 49 50 /** 51 * Mapping of source id's to one or more source-actor's. 52 * Dictionary whose keys are source id's and values are arrays 53 * made of all the related source-actor's. 54 * Note: The source mapped here are only generated sources. 55 * 56 * "source" are the objects stored in this reducer, in the `sources` attribute. 57 * "source-actor" are the objects stored in the "source-actors.js" reducer, in its `sourceActors` attribute. 58 * 59 * Map(source id => array<Source Actor object>) 60 */ 61 mutableSourceActors: new Map(), 62 63 /** 64 * All currently available sources. 65 * 66 * See create.js: `createSourceObject` method for the description of stored objects. 67 */ 68 mutableSources: new Map(), 69 70 /** 71 * All sources associated with a given URL. When using source maps, multiple 72 * sources can have the same URL. 73 * 74 * Map(url => array<source>) 75 */ 76 mutableSourcesPerUrl: new Map(), 77 78 /** 79 * When we want to select a source that isn't available yet, use this. 80 * The location object should have a url attribute instead of a sourceId. 81 * 82 * See `createPendingSelectedLocation` for the definition of this object. 83 */ 84 pendingSelectedLocation: prefs.pendingSelectedLocation, 85 86 /** 87 * The actual currently selected location. 88 * Only set if the related source is already registered in the sources reducer. 89 * Otherwise, pendingSelectedLocation should be used. Typically for sources 90 * which are about to be created. 91 * 92 * It also includes line and column information. 93 * 94 * See `createLocation` for the definition of this object. 95 */ 96 selectedLocation: undefined, 97 98 /** 99 * When selectedLocation refers to a generated source mapping to an original source 100 * via a source-map, refers to the related original location. 101 * 102 * This is UNDEFINED_LOCATION by default and will switch to NO_LOCATION asynchronously after location 103 * selection if there is no valid original location to map to. 104 */ 105 selectedOriginalLocation: UNDEFINED_LOCATION, 106 107 /** 108 * By default, the `selectedLocation` should be highlighted in the editor with a special background. 109 * On demand, this flag can be set to false in order to prevent this. 110 * The location will be shown, but not highlighted. 111 */ 112 shouldHighlightSelectedLocation: true, 113 114 /** 115 * By default, if we have a source-mapped source, we would automatically try 116 * to select and show the content of the original source. But, if we explicitly 117 * select a generated source, we remember this choice. That, until we explicitly 118 * select an original source. 119 * Note that selections related to non-source-mapped sources should never 120 * change this setting. 121 */ 122 shouldSelectOriginalLocation: true, 123 }; 124 /* eslint-disable sort-keys */ 125 } 126 127 function update(state = initialSourcesState(), action) { 128 switch (action.type) { 129 case "ADD_SOURCES": 130 return addSources(state, action.sources); 131 132 case "ADD_ORIGINAL_SOURCES": 133 return addSources(state, action.originalSources); 134 135 case "INSERT_SOURCE_ACTORS": 136 return insertSourceActors(state, action); 137 138 case "SET_SELECTED_LOCATION": { 139 let pendingSelectedLocation = null; 140 141 if (action.location.source.url) { 142 pendingSelectedLocation = createPendingSelectedLocation( 143 action.location 144 ); 145 prefs.pendingSelectedLocation = pendingSelectedLocation; 146 } 147 148 return { 149 ...state, 150 selectedLocation: action.location, 151 selectedOriginalLocation: UNDEFINED_LOCATION, 152 pendingSelectedLocation, 153 shouldSelectOriginalLocation: action.shouldSelectOriginalLocation, 154 shouldHighlightSelectedLocation: action.shouldHighlightSelectedLocation, 155 shouldScrollToSelectedLocation: action.shouldScrollToSelectedLocation, 156 }; 157 } 158 159 case "CLEAR_SELECTED_LOCATION": { 160 const pendingSelectedLocation = { url: "" }; 161 prefs.pendingSelectedLocation = pendingSelectedLocation; 162 163 return { 164 ...state, 165 selectedLocation: null, 166 selectedOriginalLocation: UNDEFINED_LOCATION, 167 pendingSelectedLocation, 168 }; 169 } 170 171 case "SET_ORIGINAL_SELECTED_LOCATION": { 172 if (action.location != state.selectedLocation) { 173 return state; 174 } 175 return { 176 ...state, 177 selectedOriginalLocation: action.originalLocation, 178 }; 179 } 180 181 case "SET_GENERATED_SELECTED_LOCATION": { 182 if (action.location != state.selectedLocation) { 183 return state; 184 } 185 return { 186 ...state, 187 selectedGeneratedLocation: action.generatedLocation, 188 }; 189 } 190 191 case "SET_DEFAULT_SELECTED_LOCATION": { 192 if ( 193 state.shouldSelectOriginalLocation == 194 action.shouldSelectOriginalLocation 195 ) { 196 return state; 197 } 198 return { 199 ...state, 200 shouldSelectOriginalLocation: action.shouldSelectOriginalLocation, 201 }; 202 } 203 204 case "SET_PENDING_SELECTED_LOCATION": { 205 const pendingSelectedLocation = { 206 url: action.url, 207 line: action.line, 208 column: action.column, 209 }; 210 211 prefs.pendingSelectedLocation = pendingSelectedLocation; 212 return { ...state, pendingSelectedLocation }; 213 } 214 215 case "SET_ORIGINAL_BREAKABLE_LINES": { 216 state.mutableOriginalBreakableLines.set( 217 action.source.id, 218 action.promise || action.breakableLines 219 ); 220 221 return { 222 ...state, 223 }; 224 } 225 226 case "ADD_BREAKPOINT_POSITIONS": { 227 // Merge existing and new reported position if some where already stored 228 let positions = state.mutableBreakpointPositions.get(action.source.id); 229 if (positions) { 230 positions = { ...positions, ...action.positions }; 231 } else { 232 positions = action.positions; 233 } 234 235 state.mutableBreakpointPositions.set(action.source.id, positions); 236 237 return { 238 ...state, 239 }; 240 } 241 242 case "CLEAR_BREAKPOINT_POSITIONS": { 243 if (!state.mutableBreakpointPositions.has(action.source.id)) { 244 return state; 245 } 246 247 state.mutableBreakpointPositions.delete(action.source.id); 248 249 return { 250 ...state, 251 }; 252 } 253 254 case "REMOVE_SOURCES": { 255 return removeSourcesAndActors(state, action); 256 } 257 } 258 259 return state; 260 } 261 262 /* 263 * Add sources to the sources store 264 * - Add the source to the sources store 265 * - Add the source URL to the source url map 266 */ 267 function addSources(state, sources) { 268 for (const source of sources) { 269 state.mutableSources.set(source.id, source); 270 271 // Update the source url map 272 const existing = state.mutableSourcesPerUrl.get(source.url); 273 if (existing) { 274 // We never return this array from selectors as-is, 275 // we either return the first entry or lookup for a precise entry 276 // so we can mutate it. 277 existing.push(source); 278 } else { 279 state.mutableSourcesPerUrl.set(source.url, [source]); 280 } 281 282 // In case of original source, maintain the mapping of generated source to original sources map. 283 if (source.isOriginal) { 284 const generatedSourceId = source.generatedSource.id; 285 let originalSourceIds = 286 state.mutableOriginalSources.get(generatedSourceId); 287 if (!originalSourceIds) { 288 originalSourceIds = []; 289 state.mutableOriginalSources.set(generatedSourceId, originalSourceIds); 290 } 291 // We never return this array out of selectors, so mutate the list 292 originalSourceIds.push(source.id); 293 } 294 } 295 296 return { ...state }; 297 } 298 299 function removeSourcesAndActors(state, action) { 300 const { 301 mutableSourcesPerUrl, 302 mutableSources, 303 mutableOriginalSources, 304 mutableSourceActors, 305 mutableOriginalBreakableLines, 306 mutableBreakpointPositions, 307 } = state; 308 309 const newState = { ...state }; 310 311 for (const removedSource of action.sources) { 312 const sourceId = removedSource.id; 313 314 // Clear the urls Map 315 const sourceUrl = removedSource.url; 316 if (sourceUrl) { 317 const sourcesForSameUrl = ( 318 mutableSourcesPerUrl.get(sourceUrl) || [] 319 ).filter(s => s != removedSource); 320 if (!sourcesForSameUrl.length) { 321 // All sources with this URL have been removed 322 mutableSourcesPerUrl.delete(sourceUrl); 323 } else { 324 // There are other sources still alive with the same URL 325 mutableSourcesPerUrl.set(sourceUrl, sourcesForSameUrl); 326 } 327 } 328 329 mutableSources.delete(sourceId); 330 331 // Note that the caller of this method queried the reducer state 332 // to aggregate the related original sources. 333 // So if we were having related original sources, they will be 334 // in `action.sources`. 335 mutableOriginalSources.delete(sourceId); 336 337 // If a source is removed, immediately remove all its related source actors. 338 // It can speed-up the following for loop cleaning actors. 339 mutableSourceActors.delete(sourceId); 340 341 if (removedSource.isOriginal) { 342 mutableOriginalBreakableLines.delete(sourceId); 343 // Also ensure removing this original source id in the array specific to its 344 // generated source 345 const generatedSourceId = removedSource.generatedSource.id; 346 let originalSourceIds = mutableOriginalSources.get(generatedSourceId); 347 if (originalSourceIds) { 348 originalSourceIds = originalSourceIds.filter(id => id != sourceId); 349 mutableOriginalSources.set(generatedSourceId, originalSourceIds); 350 } 351 352 // We should also remove the mapped location from the breakpoint positions 353 // 354 // `mutableBreakpointPositions` is a Map keyed per generated source id 355 // `generatedBreakpointPositions` is a Array 356 // `position` is an object with `location` and `generatedLocation` attributes 357 const generatedBreakpointPositions = 358 mutableBreakpointPositions.get(generatedSourceId); 359 if (generatedBreakpointPositions) { 360 for (const line in generatedBreakpointPositions) { 361 for (const position of generatedBreakpointPositions[line]) { 362 // Only clear the original mapped location if that's a breakpoint 363 // for the currently removed original source. This generated/bundle source 364 // may have breakpoints for many original sources. 365 if (position.location.source == removedSource) { 366 position.location = position.generatedLocation; 367 } 368 } 369 } 370 } 371 } 372 373 mutableBreakpointPositions.delete(sourceId); 374 375 if ( 376 action.resetSelectedLocation && 377 newState.selectedLocation?.source == removedSource 378 ) { 379 newState.selectedLocation = null; 380 newState.selectedOriginalLocation = UNDEFINED_LOCATION; 381 } 382 } 383 384 for (const removedActor of action.actors) { 385 const sourceId = removedActor.source; 386 const actorsForSource = mutableSourceActors.get(sourceId); 387 // actors may have already been cleared by the previous for..loop 388 if (!actorsForSource) { 389 continue; 390 } 391 const idx = actorsForSource.indexOf(removedActor); 392 if (idx != -1) { 393 actorsForSource.splice(idx, 1); 394 // While the Map is mutable, we expect new array instance on each new change 395 mutableSourceActors.set(sourceId, [...actorsForSource]); 396 } 397 398 // Remove the entry in the Map if there is no more actors for that source 399 if (!actorsForSource.length) { 400 mutableSourceActors.delete(sourceId); 401 } 402 403 if ( 404 action.resetSelectedLocation && 405 newState.selectedLocation?.sourceActor == removedActor 406 ) { 407 newState.selectedLocation = null; 408 newState.selectedOriginalLocation = UNDEFINED_LOCATION; 409 } 410 } 411 412 return newState; 413 } 414 415 function insertSourceActors(state, action) { 416 const { sourceActors } = action; 417 418 const { mutableSourceActors } = state; 419 // The `sourceActor` objects are defined from `newGeneratedSources` action: 420 // https://searchfox.org/mozilla-central/rev/4646b826a25d3825cf209db890862b45fa09ffc3/devtools/client/debugger/src/actions/sources/newSources.js#300-314 421 for (const sourceActor of sourceActors) { 422 const sourceId = sourceActor.source; 423 // We always clone the array of source actors as we return it from selectors. 424 // So the map is mutable, but its values are considered immutable and will change 425 // anytime there is a new actor added per source ID. 426 const existing = mutableSourceActors.get(sourceId); 427 if (existing) { 428 mutableSourceActors.set(sourceId, [...existing, sourceActor]); 429 } else { 430 mutableSourceActors.set(sourceId, [sourceActor]); 431 } 432 } 433 434 const scriptActors = sourceActors.filter( 435 item => item.introductionType === "scriptElement" 436 ); 437 if (scriptActors.length) { 438 // If new HTML sources are being added, we need to clear the breakpoint 439 // positions since the new source is a <script> with new breakpoints. 440 for (const { source } of scriptActors) { 441 state.mutableBreakpointPositions.delete(source); 442 } 443 } 444 445 return { ...state }; 446 } 447 448 export default update;