sources.js (12342B)
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 import { createSelector } from "devtools/client/shared/vendor/reselect"; 6 7 import { getPrettySourceURL, isJavaScript } from "../utils/source"; 8 9 import { findPosition } from "../utils/breakpoint/breakpointPositions"; 10 import { isFulfilled } from "../utils/async-value"; 11 12 import { prefs } from "../utils/prefs"; 13 import { UNDEFINED_LOCATION, NO_LOCATION } from "../reducers/sources"; 14 15 import { 16 hasSourceActor, 17 getSourceActor, 18 getBreakableLinesForSourceActors, 19 isSourceActorWithSourceMap, 20 } from "./source-actors"; 21 import { 22 getSourceTextContentForLocation, 23 getSourceTextContentForSource, 24 } from "./sources-content"; 25 26 export function hasSource(state, id) { 27 return state.sources.mutableSources.has(id); 28 } 29 30 export function getSource(state, id) { 31 return state.sources.mutableSources.get(id); 32 } 33 34 export function getSourceFromId(state, id) { 35 const source = getSource(state, id); 36 if (!source) { 37 console.warn(`source ${id} does not exist`); 38 dump(`>> ${new Error().stack}\n`); 39 } 40 return source; 41 } 42 43 export function getSourceByActorId(state, actorId) { 44 if (!hasSourceActor(state, actorId)) { 45 return null; 46 } 47 48 return getSource(state, getSourceActor(state, actorId).source); 49 } 50 51 function getSourcesByURL(state, url) { 52 return state.sources.mutableSourcesPerUrl.get(url) || []; 53 } 54 55 export function getSourceByURL(state, url) { 56 const foundSources = getSourcesByURL(state, url); 57 return foundSources[0]; 58 } 59 60 // This is used by tabs selectors 61 export function getSpecificSourceByURL(state, url, isOriginal) { 62 const foundSources = getSourcesByURL(state, url); 63 return foundSources.find(source => source.isOriginal == isOriginal); 64 } 65 66 function getOriginalSourceByURL(state, url) { 67 return getSpecificSourceByURL(state, url, true); 68 } 69 70 export function getGeneratedSourceByURL(state, url) { 71 return getSpecificSourceByURL(state, url, false); 72 } 73 74 export function getPendingSelectedLocation(state) { 75 return state.sources.pendingSelectedLocation; 76 } 77 78 export function getPrettySource(state, id) { 79 if (!id) { 80 return null; 81 } 82 83 const source = getSource(state, id); 84 if (!source) { 85 return null; 86 } 87 88 return getOriginalSourceByURL(state, getPrettySourceURL(source.url)); 89 } 90 91 // This is only used by Project Search and tests. 92 export function getSourceList(state) { 93 return [...state.sources.mutableSources.values()]; 94 } 95 96 // This is only used by tests and create.js 97 export function getSourceCount(state) { 98 return state.sources.mutableSources.size; 99 } 100 101 export function getSelectedLocation(state) { 102 return state.sources.selectedLocation; 103 } 104 105 /** 106 * Return the "mapped" location for the currently selected location: 107 * - When selecting a location in an original source, returns 108 * the related location in the bundle source. 109 * 110 * - When selecting a location in a bundle source, returns 111 * the related location in the original source. This may return undefined 112 * while we are still computing this information. (we need to query the asynchronous SourceMap service) 113 * 114 * - Otherwise, when selecting a location in a source unrelated to source map 115 * or a pretty printed source, returns null. 116 */ 117 export function getSelectedMappedSource(state) { 118 const selectedLocation = getSelectedLocation(state); 119 if (!selectedLocation) { 120 return null; 121 } 122 123 // Don't map pretty printed to its related compressed source 124 if (selectedLocation.source.isPrettyPrinted) { 125 return null; 126 } 127 128 // If we are on a bundle with a functional source-map, 129 // the `selectLocation` action should compute the `selectedOriginalLocation` field. 130 if ( 131 !selectedLocation.source.isOriginal && 132 isSourceActorWithSourceMap(state, selectedLocation.sourceActor.id) 133 ) { 134 const { selectedOriginalLocation } = state.sources; 135 // Return undefined if we are still loading the source map. 136 // `selectedOriginalLocation` will be set to undefined instead of null 137 if ( 138 selectedOriginalLocation && 139 selectedOriginalLocation != UNDEFINED_LOCATION && 140 selectedOriginalLocation != NO_LOCATION 141 ) { 142 return selectedOriginalLocation.source; 143 } 144 return null; 145 } 146 147 // For non original source, which don't have selectedOriginalLocation provided, 148 // don't try to map to anything. 149 if (!selectedLocation.source.isOriginal) { 150 return null; 151 } 152 153 // Otherwise, for original source, simply map to their related generated source 154 return selectedLocation.source.generatedSource; 155 } 156 157 /** 158 * Helps knowing if we are still computing the mapped location for the currently selected source. 159 */ 160 export function isSelectedMappedSourceLoading(state) { 161 const { selectedOriginalLocation } = state.sources; 162 // This `selectedOriginalLocation` attribute is set to UNDEFINED_LOCATION when selecting a new source attribute 163 // and later on, when the source map is processed, it will switch to either a valid location object, or NO_LOCATION if no valid one if found. 164 return selectedOriginalLocation === UNDEFINED_LOCATION; 165 } 166 167 export const getSelectedSource = createSelector( 168 getSelectedLocation, 169 selectedLocation => { 170 if (!selectedLocation) { 171 return undefined; 172 } 173 174 return selectedLocation.source; 175 } 176 ); 177 178 // This is used by tests and pause reducers 179 export function getSelectedSourceId(state) { 180 const source = getSelectedSource(state); 181 return source?.id; 182 } 183 184 export function getShouldSelectOriginalLocation(state) { 185 return state.sources.shouldSelectOriginalLocation; 186 } 187 188 export function getShouldHighlightSelectedLocation(state) { 189 return state.sources.shouldHighlightSelectedLocation; 190 } 191 192 export function getShouldScrollToSelectedLocation(state) { 193 return state.sources.shouldScrollToSelectedLocation; 194 } 195 196 /** 197 * Gets the first source actor for the source and/or thread 198 * provided. 199 * 200 * @param {object} state 201 * @param {string} sourceId 202 * The source used 203 * @param {string} [threadId] 204 * The thread to check, this is optional. 205 * @param {object} sourceActor 206 */ 207 export function getFirstSourceActorForGeneratedSource( 208 state, 209 sourceId, 210 threadId 211 ) { 212 let source = getSource(state, sourceId); 213 // The source may have been removed if we are being called by async code 214 if (!source) { 215 return null; 216 } 217 if (source.isOriginal) { 218 source = source.generatedSource; 219 } 220 const actors = getSourceActorsForSource(state, source.id); 221 if (threadId) { 222 return actors.find(actorInfo => actorInfo.thread == threadId) || null; 223 } 224 return actors[0] || null; 225 } 226 227 /** 228 * Get the source actor of the source 229 * 230 * @param {object} state 231 * @param {string} id 232 * The source id 233 * @return {Array<object>} 234 * List of source actors 235 */ 236 export function getSourceActorsForSource(state, id) { 237 return state.sources.mutableSourceActors.get(id) || []; 238 } 239 240 export function isSourceWithMap(state, id) { 241 const actors = getSourceActorsForSource(state, id); 242 return actors.some(actor => isSourceActorWithSourceMap(state, actor.id)); 243 } 244 245 export function canPrettyPrintSource(state, source, sourceActor) { 246 if ( 247 !source || 248 source.isPrettyPrinted || 249 source.isOriginal || 250 (prefs.clientSourceMapsEnabled && isSourceWithMap(state, source.id)) 251 ) { 252 return false; 253 } 254 255 const content = getSourceTextContentForSource(state, source, sourceActor); 256 const sourceContent = content && isFulfilled(content) ? content.value : null; 257 258 if ( 259 !sourceContent || 260 (!isJavaScript(source, sourceContent) && !source.isHTML) 261 ) { 262 return false; 263 } 264 265 return true; 266 } 267 268 export function getPrettyPrintMessage(state, location) { 269 const source = location.source; 270 if (!source) { 271 return L10N.getStr("sourceTabs.prettyPrint"); 272 } 273 274 if (source.isPrettyPrinted) { 275 return L10N.getStr("sourceTabs.removePrettyPrint"); 276 } 277 278 if (source.isOriginal) { 279 return L10N.getStr("sourceFooter.prettyPrint.isOriginalMessage"); 280 } 281 282 if (prefs.clientSourceMapsEnabled && isSourceWithMap(state, source.id)) { 283 return L10N.getStr("sourceFooter.prettyPrint.hasSourceMapMessage"); 284 } 285 286 const content = getSourceTextContentForLocation(state, location); 287 288 const sourceContent = content && isFulfilled(content) ? content.value : null; 289 if (!sourceContent) { 290 return L10N.getStr("sourceFooter.prettyPrint.noContentMessage"); 291 } 292 293 if (!isJavaScript(source, sourceContent) && !source.isHTML) { 294 return L10N.getStr("sourceFooter.prettyPrint.isNotJavascriptMessage"); 295 } 296 297 return L10N.getStr("sourceTabs.prettyPrint"); 298 } 299 300 export function getBreakpointPositionsForSource(state, sourceId) { 301 return state.sources.mutableBreakpointPositions.get(sourceId); 302 } 303 304 // This is only used by one test 305 export function hasBreakpointPositions(state, sourceId) { 306 return !!getBreakpointPositionsForSource(state, sourceId); 307 } 308 309 export function getBreakpointPositionsForLine(state, sourceId, line) { 310 const positions = getBreakpointPositionsForSource(state, sourceId); 311 return positions?.[line]; 312 } 313 314 export function getBreakpointPositionsForLocation(state, location) { 315 const sourceId = location.source.id; 316 const positions = getBreakpointPositionsForSource(state, sourceId); 317 return findPosition(positions, location); 318 } 319 320 export function getBreakableLines(state, sourceId) { 321 if (!sourceId) { 322 return null; 323 } 324 const source = getSource(state, sourceId); 325 if (!source) { 326 return null; 327 } 328 329 if (source.isOriginal) { 330 return state.sources.mutableOriginalBreakableLines.get(sourceId); 331 } 332 333 const sourceActors = getSourceActorsForSource(state, sourceId); 334 if (!sourceActors.length) { 335 return null; 336 } 337 338 // We pull generated file breakable lines directly from the source actors 339 // so that breakable lines can be added as new source actors on HTML loads. 340 return getBreakableLinesForSourceActors(state, sourceActors, source.isHTML); 341 } 342 343 export const getSelectedBreakableLines = createSelector( 344 state => { 345 const sourceId = getSelectedSourceId(state); 346 if (!sourceId) { 347 return null; 348 } 349 const breakableLines = getBreakableLines(state, sourceId); 350 // Ignore the breakable lines if they are still being fetched from the server 351 if (!breakableLines || breakableLines instanceof Promise) { 352 return null; 353 } 354 return breakableLines; 355 }, 356 breakableLines => new Set(breakableLines || []) 357 ); 358 359 export function isSourceOverridden(toolboxState, source) { 360 if (!source || !source.url) { 361 return false; 362 } 363 return !!toolboxState.networkOverrides.mutableOverrides[source.url]; 364 } 365 366 /** 367 * Compute the list of source actors and source objects to be removed 368 * when removing a given target/thread. 369 * 370 * @param {string} threadActorID 371 * The thread to be removed. 372 * @return {object} 373 * An object with two arrays: 374 * - actors: list of source actor objects to remove 375 * - sources: list of source objects to remove 376 */ 377 export function getSourcesToRemoveForThread(state, threadActorID) { 378 const sourcesToRemove = []; 379 const actorsToRemove = []; 380 381 for (const [ 382 sourceId, 383 actorsForSource, 384 ] of state.sources.mutableSourceActors.entries()) { 385 let removedActorsCount = 0; 386 // Find all actors for the current source which belongs to the given thread actor 387 for (const actor of actorsForSource) { 388 if (actor.thread == threadActorID) { 389 actorsToRemove.push(actor); 390 removedActorsCount++; 391 } 392 } 393 394 // If we are about to remove all source actors for the current source, 395 // or if for some unexpected reason we have a source with no actors, 396 // notify the caller to also remove this source. 397 if ( 398 removedActorsCount == actorsForSource.length || 399 !actorsForSource.length 400 ) { 401 sourcesToRemove.push(state.sources.mutableSources.get(sourceId)); 402 403 // Also remove any original sources related to this generated source 404 const originalSourceIds = 405 state.sources.mutableOriginalSources.get(sourceId); 406 if (originalSourceIds?.length > 0) { 407 for (const originalSourceId of originalSourceIds) { 408 sourcesToRemove.push( 409 state.sources.mutableSources.get(originalSourceId) 410 ); 411 } 412 } 413 } 414 } 415 416 return { 417 actors: actorsToRemove, 418 sources: sourcesToRemove, 419 }; 420 }