summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/actions/breakpoints/syncBreakpoint.js
blob: aec56664d086918c0438b61edb820f81c3d2591e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/* 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/>. */

import { setBreakpointPositions } from "./breakpointPositions";
import {
  findPosition,
  makeBreakpointServerLocation,
} from "../../utils/breakpoint/index";

import { comparePosition, createLocation } from "../../utils/location";

import { originalToGeneratedId } from "devtools/client/shared/source-map-loader/index";
import { getSource } from "../../selectors/index";
import { addBreakpoint, removeBreakpointAtGeneratedLocation } from "./modify";

async function findBreakpointPosition({ dispatch }, location) {
  const positions = await dispatch(setBreakpointPositions(location));

  const position = findPosition(positions, location);
  return position;
}

// Breakpoint syncing occurs when a source is found that matches either the
// original or generated URL of a pending breakpoint. A new breakpoint is
// constructed that might have a different original and/or generated location,
// if the original source has changed since the pending breakpoint was created.
// There are a couple subtle aspects to syncing:
//
// - We handle both the original and generated source because there is no
//   guarantee that seeing the generated source means we will also see the
//   original source. When connecting, a breakpoint will be installed in the
//   client for the generated location in the pending breakpoint, and we need
//   to make sure that either a breakpoint is added to the reducer or that this
//   client breakpoint is deleted.
//
// - If we see both the original and generated sources and the source mapping
//   has changed, we need to make sure that only a single breakpoint is added
//   to the reducer for the new location corresponding to the original location
//   in the pending breakpoint.
export function syncPendingBreakpoint(source, pendingBreakpoint) {
  return async thunkArgs => {
    const { getState, client, dispatch } = thunkArgs;

    const generatedSourceId = source.isOriginal
      ? originalToGeneratedId(source.id)
      : source.id;

    const generatedSource = getSource(getState(), generatedSourceId);

    if (!source || !generatedSource) {
      return null;
    }

    // /!\ Pending breakpoint locations come only with sourceUrl, line and column attributes.
    // We have to map it to a specific source object and avoid trying to query its non-existent 'source' attribute.
    const { location, generatedLocation } = pendingBreakpoint;
    const isPendingBreakpointWithSourceMap =
      location.sourceUrl != generatedLocation.sourceUrl;
    const sourceGeneratedLocation = createLocation({
      ...generatedLocation,
      source: generatedSource,
    });

    if (source == generatedSource && isPendingBreakpointWithSourceMap) {
      // We are handling the generated source and the pending breakpoint has a
      // source mapping. Supply a cancellation callback that will abort the
      // breakpoint if the original source was synced to a different location,
      // in which case the client breakpoint has been removed.
      const breakpointServerLocation = makeBreakpointServerLocation(
        getState(),
        sourceGeneratedLocation
      );
      return dispatch(
        addBreakpoint(
          sourceGeneratedLocation,
          pendingBreakpoint.options,
          pendingBreakpoint.disabled,
          () => !client.hasBreakpoint(breakpointServerLocation)
        )
      );
    }

    const originalLocation = createLocation({
      ...location,
      source,
    });

    const newPosition = await findBreakpointPosition(
      thunkArgs,
      originalLocation
    );

    const newGeneratedLocation = newPosition?.generatedLocation;
    if (!newGeneratedLocation) {
      // We couldn't find a new mapping for the breakpoint. If there is a source
      // mapping, remove any breakpoints for the generated location, as if the
      // breakpoint moved. If the old generated location still maps to an
      // original location then we don't want to add a breakpoint for it.
      if (isPendingBreakpointWithSourceMap) {
        dispatch(removeBreakpointAtGeneratedLocation(sourceGeneratedLocation));
      }
      return null;
    }

    const isSameLocation = comparePosition(
      generatedLocation,
      newGeneratedLocation
    );

    // If the new generated location has changed from that in the pending
    // breakpoint, remove any breakpoint associated with the old generated
    // location.
    if (!isSameLocation) {
      dispatch(removeBreakpointAtGeneratedLocation(sourceGeneratedLocation));
    }

    return dispatch(
      addBreakpoint(
        newGeneratedLocation,
        pendingBreakpoint.options,
        pendingBreakpoint.disabled
      )
    );
  };
}