summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/actions/pause/inlinePreview.js
blob: 4f32ff6292996721a3a225fa4ca576745e8a24bb (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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
/* 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 {
  getOriginalFrameScope,
  getGeneratedFrameScope,
  getInlinePreviews,
  getSelectedLocation,
} from "../../selectors/index";
import { features } from "../../utils/prefs";
import { validateSelectedFrame } from "../../utils/context";

// We need to display all variables in the current functional scope so
// include all data for block scopes until the first functional scope
function getLocalScopeLevels(originalAstScopes) {
  let levels = 0;
  while (
    originalAstScopes[levels] &&
    originalAstScopes[levels].type === "block"
  ) {
    levels++;
  }
  return levels;
}

export function generateInlinePreview(selectedFrame) {
  return async function ({ dispatch, getState, parserWorker, client }) {
    if (!features.inlinePreview) {
      return null;
    }

    // Avoid regenerating inline previews when we already have preview data
    if (getInlinePreviews(getState(), selectedFrame.thread, selectedFrame.id)) {
      return null;
    }

    const originalFrameScopes = getOriginalFrameScope(
      getState(),
      selectedFrame
    );

    const generatedFrameScopes = getGeneratedFrameScope(
      getState(),
      selectedFrame
    );

    let scopes = originalFrameScopes?.scope || generatedFrameScopes?.scope;

    if (!scopes || !scopes.bindings) {
      return null;
    }

    // It's important to use selectedLocation, because we don't know
    // if we'll be viewing the original or generated frame location
    const selectedLocation = getSelectedLocation(getState());
    if (!selectedLocation) {
      return null;
    }

    if (!parserWorker.isLocationSupported(selectedLocation)) {
      return null;
    }

    const originalAstScopes = await parserWorker.getScopes(selectedLocation);
    validateSelectedFrame(getState(), selectedFrame);

    if (!originalAstScopes) {
      return null;
    }

    const allPreviews = [];
    const pausedOnLine = selectedLocation.line;
    const levels = getLocalScopeLevels(originalAstScopes);

    for (
      let curLevel = 0;
      curLevel <= levels && scopes && scopes.bindings;
      curLevel++
    ) {
      const bindings = { ...scopes.bindings.variables };
      scopes.bindings.arguments.forEach(argument => {
        Object.keys(argument).forEach(key => {
          bindings[key] = argument[key];
        });
      });

      const previewBindings = Object.keys(bindings).map(async name => {
        // We want to show values of properties of objects only and not
        // function calls on other data types like someArr.forEach etc..
        let properties = null;
        const objectGrip = bindings[name].value;
        if (objectGrip.actor && objectGrip.class === "Object") {
          properties = await client.loadObjectProperties(
            {
              name,
              path: name,
              contents: { value: objectGrip },
            },
            selectedFrame.thread
          );
        }

        const previewsFromBindings = getBindingValues(
          originalAstScopes,
          pausedOnLine,
          name,
          bindings[name].value,
          curLevel,
          properties
        );

        allPreviews.push(...previewsFromBindings);
      });
      await Promise.all(previewBindings);

      scopes = scopes.parent;
    }

    // Sort previews by line and column so they're displayed in the right order in the editor
    allPreviews.sort((previewA, previewB) => {
      if (previewA.line < previewB.line) {
        return -1;
      }
      if (previewA.line > previewB.line) {
        return 1;
      }
      // If we have the same line number
      return previewA.column < previewB.column ? -1 : 1;
    });

    const previews = {};
    for (const preview of allPreviews) {
      const { line } = preview;
      if (!previews[line]) {
        previews[line] = [];
      }
      previews[line].push(preview);
    }

    return dispatch({
      type: "ADD_INLINE_PREVIEW",
      selectedFrame,
      previews,
    });
  };
}

function getBindingValues(
  originalAstScopes,
  pausedOnLine,
  name,
  value,
  curLevel,
  properties
) {
  const previews = [];

  const binding = originalAstScopes[curLevel]?.bindings[name];
  if (!binding) {
    return previews;
  }

  // Show a variable only once ( an object and it's child property are
  // counted as different )
  const identifiers = new Set();

  // We start from end as we want to show values besides variable
  // located nearest to the breakpoint
  for (let i = binding.refs.length - 1; i >= 0; i--) {
    const ref = binding.refs[i];
    // Subtracting 1 from line as codemirror lines are 0 indexed
    const line = ref.start.line - 1;
    const column = ref.start.column;
    // We don't want to render inline preview below the paused line
    if (line >= pausedOnLine - 1) {
      continue;
    }

    const { displayName, displayValue } = getExpressionNameAndValue(
      name,
      value,
      ref,
      properties
    );

    // Variable with same name exists, display value of current or
    // closest to the current scope's variable
    if (identifiers.has(displayName)) {
      continue;
    }
    identifiers.add(displayName);

    previews.push({
      line,
      column,
      name: displayName,
      value: displayValue,
    });
  }
  return previews;
}

function getExpressionNameAndValue(
  name,
  value,
  // TODO: Add data type to ref
  ref,
  properties
) {
  let displayName = name;
  let displayValue = value;

  // Only variables of type Object will have properties
  if (properties) {
    let { meta } = ref;
    // Presence of meta property means expression contains child property
    // reference eg: objName.propName
    while (meta) {
      // Initially properties will be an array, after that it will be an object
      if (displayValue === value) {
        const property = properties.find(prop => prop.name === meta.property);
        displayValue = property?.contents.value;
        displayName += `.${meta.property}`;
      } else if (displayValue?.preview?.ownProperties) {
        const { ownProperties } = displayValue.preview;
        Object.keys(ownProperties).forEach(prop => {
          if (prop === meta.property) {
            displayValue = ownProperties[prop].value;
            displayName += `.${meta.property}`;
          }
        });
      }
      meta = meta.parent;
    }
  }

  return { displayName, displayValue };
}